diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-18 05:52:35 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-18 05:52:35 +0000 |
commit | 7fec0b69a082aaeec72fee0612766aa42f6b1b4d (patch) | |
tree | efb569b86ca4da888717f5433e757145fa322e08 /ansible_collections/awx | |
parent | Releasing progress-linux version 7.7.0+dfsg-3~progress7.99u1. (diff) | |
download | ansible-7fec0b69a082aaeec72fee0612766aa42f6b1b4d.tar.xz ansible-7fec0b69a082aaeec72fee0612766aa42f6b1b4d.zip |
Merging upstream version 9.4.0+dfsg.
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'ansible_collections/awx')
100 files changed, 3386 insertions, 1465 deletions
diff --git a/ansible_collections/awx/awx/FILES.json b/ansible_collections/awx/awx/FILES.json index 9a0b1d45c..9fba1d572 100644 --- a/ansible_collections/awx/awx/FILES.json +++ b/ansible_collections/awx/awx/FILES.json @@ -8,647 +8,703 @@ "format": 1 }, { - "name": "bindep.txt", + "name": "README.md", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "7205dda85d2cd5501b3344e9f18e4acd09583056aab5e8a05554ba29a3b8fad8", + "chksum_sha256": "41997bded547ba6a168ee41fd5379a44f20dcd155979067564b79d14af6e3fd3", "format": 1 }, { - "name": "COPYING", + "name": "meta", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "meta/runtime.yml", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "7c50cd9b85e2b7eebaea2b5618b402862b01d5a66befff8e41401ef3f14e471a", + "chksum_sha256": "6c63b75e8e4c9b744914c111d3ff8b54f0e973b78ce444815acfb4aa254a9ad6", "format": 1 }, { - "name": "images", + "name": "bindep.txt", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "7205dda85d2cd5501b3344e9f18e4acd09583056aab5e8a05554ba29a3b8fad8", + "format": 1 + }, + { + "name": "plugins", "ftype": "dir", "chksum_type": null, "chksum_sha256": null, "format": 1 }, { - "name": "images/completeness_test_output.png", + "name": "plugins/doc_fragments", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "plugins/doc_fragments/auth_plugin.py", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "6367684c4b5edd3e1e8fdcb9270d68ca54040d5d17108734f3d3a2b9df5878ba", + "chksum_sha256": "f1c61a9880edca852582f58c635c32264b0d1b3218ebdeb9db9d23081267c070", "format": 1 }, { - "name": "README.md", + "name": "plugins/doc_fragments/auth.py", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "490a000bf64790d90206607e3dd74c77e7ae940e58346a319a590053afe72149", + "chksum_sha256": "83f0ecfb936c89bd2b589eff8710617080f539e943f38bf57b45b4eb753564e8", "format": 1 }, { - "name": "TESTING.md", + "name": "plugins/doc_fragments/auth_legacy.py", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "4691e79c8038d8e985610fb613cd2f4799d4740b0a6ca1b72d3266528088a272", + "chksum_sha256": "c2f10b81ecb89088c7c295430d4a71de26e3700b26e8344cdc7950908a738fd3", "format": 1 }, { - "name": "tests", + "name": "plugins/inventory", "ftype": "dir", "chksum_type": null, "chksum_sha256": null, "format": 1 }, { - "name": "tests/integration", - "ftype": "dir", - "chksum_type": null, - "chksum_sha256": null, + "name": "plugins/inventory/controller.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "29e7cd36a2b18ee616e31cbbec6a6e103f3f18ebe13f2bb87167b915163ca4bf", "format": 1 }, { - "name": "tests/integration/targets", + "name": "plugins/modules", "ftype": "dir", "chksum_type": null, "chksum_sha256": null, "format": 1 }, { - "name": "tests/integration/targets/instance_group", - "ftype": "dir", - "chksum_type": null, - "chksum_sha256": null, + "name": "plugins/modules/job_cancel.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "d64f698909919b05c9c47a65f24c861c3cabe33c039944f6120d49a2ac7d40da", "format": 1 }, { - "name": "tests/integration/targets/instance_group/tasks", - "ftype": "dir", - "chksum_type": null, - "chksum_sha256": null, + "name": "plugins/modules/ad_hoc_command_cancel.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "3338e10af9ccd0e4178b8e1ec1e7064b00ab90e64665f846a2123f10d9d151f4", "format": 1 }, { - "name": "tests/integration/targets/instance_group/tasks/main.yml", + "name": "plugins/modules/workflow_approval.py", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "2498af3df7c31531b3d029afea14945fc211c6cc8a535a855330fb699e7a7d32", + "chksum_sha256": "533e52fc20ca99e935154f5ba3ec30c2055f42d1c51c3bc2cf1570af7f951c33", "format": 1 }, { - "name": "tests/integration/targets/bulk_host_create", - "ftype": "dir", - "chksum_type": null, - "chksum_sha256": null, + "name": "plugins/modules/job_list.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "2ea8024bfc9612c005745a13a508c40d320b4c204bf18fcd495f72789d9adb40", "format": 1 }, { - "name": "tests/integration/targets/bulk_host_create/main.yml", + "name": "plugins/modules/bulk_host_delete.py", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "39be6c097c03152ec1873732d8e09e34639464acad4f9d42ef3e13b6f87d47d1", + "chksum_sha256": "546fb1eb2104db87c1d608144590e38d361af2ba734caa62fc61586e49a124ad", "format": 1 }, { - "name": "tests/integration/targets/ad_hoc_command_cancel", - "ftype": "dir", - "chksum_type": null, - "chksum_sha256": null, + "name": "plugins/modules/inventory_source_update.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "e0b79d76d7d9f817f709a8dacbcb2a105a214c33e63449decaec65adebac6d74", "format": 1 }, { - "name": "tests/integration/targets/ad_hoc_command_cancel/tasks", - "ftype": "dir", - "chksum_type": null, - "chksum_sha256": null, + "name": "plugins/modules/workflow_launch.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "3aff0ecdf76b65df3e17f7c8a34eddae8a1bc4d35c304619f7c2054927584d35", "format": 1 }, { - "name": "tests/integration/targets/ad_hoc_command_cancel/tasks/main.yml", + "name": "plugins/modules/controller_meta.py", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "3f698a655089b977ee89dde6532823b4e496b190a0203b52e75e0a19b0321e3f", + "chksum_sha256": "c66ebbe3a0eab6a9d28d517824ebf8478afdf14981c6c931f08592503c243cdd", "format": 1 }, { - "name": "tests/integration/targets/ad_hoc_command", - "ftype": "dir", - "chksum_type": null, - "chksum_sha256": null, + "name": "plugins/modules/credential.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "e6d52d95b7e59a41b5d3715c3b55857c1b70baf7a0e8e44e9cc66db67b87685f", "format": 1 }, { - "name": "tests/integration/targets/ad_hoc_command/tasks", - "ftype": "dir", - "chksum_type": null, - "chksum_sha256": null, + "name": "plugins/modules/team.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "e53a544c12fc2de705bce163e3babf6d95fae6d088f1415a61debeb07d60f991", "format": 1 }, { - "name": "tests/integration/targets/ad_hoc_command/tasks/main.yml", + "name": "plugins/modules/host.py", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "e77cbce44dc257461c1edb3602690be28a7d8bf5e11384c9f5f401b6e1cb3149", + "chksum_sha256": "b973c5f3790dc0c084e97a6f6b0c87209632f1ff348bc36466cc13392e774c79", "format": 1 }, { - "name": "tests/integration/targets/credential_type", - "ftype": "dir", - "chksum_type": null, - "chksum_sha256": null, + "name": "plugins/modules/job_launch.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "1c26e876232d5658537b9d041879f93e024e52c901551b5e6ad89354d6023d71", "format": 1 }, { - "name": "tests/integration/targets/credential_type/tasks", - "ftype": "dir", - "chksum_type": null, - "chksum_sha256": null, + "name": "plugins/modules/credential_input_source.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "78ce109c0cc51aa8d66f0146ac9f448559b186cc2b155ee2a8ed96cebe65909d", "format": 1 }, { - "name": "tests/integration/targets/credential_type/tasks/main.yml", + "name": "plugins/modules/instance_group.py", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "b354b1b26b90216a8460fc9d478422c825f87d0dc1d59074acdc4650e8a0fb34", + "chksum_sha256": "14c7f97d44a266a9d73d8ef1fc85f8c15ecbab611ff8133026ef943e211ba7c2", "format": 1 }, { - "name": "tests/integration/targets/project", - "ftype": "dir", - "chksum_type": null, - "chksum_sha256": null, + "name": "plugins/modules/inventory_source.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "279efe4103630b4961baeb468c43f9d75c657c0beb36ece73982d31080931403", "format": 1 }, { - "name": "tests/integration/targets/project/tasks", - "ftype": "dir", - "chksum_type": null, - "chksum_sha256": null, + "name": "plugins/modules/application.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "e1b39c7cd3d608245b7d419547d6582fca5143bf8f72ff39fa89cb67d4ee8ac0", "format": 1 }, { - "name": "tests/integration/targets/project/tasks/main.yml", + "name": "plugins/modules/role.py", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "6267e4d55fe0c8ac80f174bd90deb37ba3dea9825eef2ec9ce28f6863cfed562", + "chksum_sha256": "31aebaae562881a3a9ecea61f5d26747559cbed69dcf45fbe21d9a65412ea56c", "format": 1 }, { - "name": "tests/integration/targets/bulk_job_launch", - "ftype": "dir", - "chksum_type": null, - "chksum_sha256": null, + "name": "plugins/modules/token.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "cc35a007403827d4994793d9bb4a5d573d9b532c605feee6d97b119037ead203", "format": 1 }, { - "name": "tests/integration/targets/bulk_job_launch/main.yml", + "name": "plugins/modules/export.py", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "76a5b81caaa30a7dc5fd68048b925551a83ef2cee8a0b4c15d02f0857fbbe812", + "chksum_sha256": "b62aa153c8a819461f3bcbd27a4de731e477dffc4d05def8829687901b71aec9", "format": 1 }, { - "name": "tests/integration/targets/inventory", - "ftype": "dir", - "chksum_type": null, - "chksum_sha256": null, + "name": "plugins/modules/import.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "a7a03186251ef644ba03c49e7e23a799f8046abddb9ea20fff68dd09fe759680", "format": 1 }, { - "name": "tests/integration/targets/inventory/tasks", - "ftype": "dir", - "chksum_type": null, - "chksum_sha256": null, + "name": "plugins/modules/credential_type.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "1867c68c3b43c0f27a8e87dcbb66b22fdfc1cd659cc1747a6686573f6a7d6be9", "format": 1 }, { - "name": "tests/integration/targets/inventory/tasks/main.yml", + "name": "plugins/modules/workflow_job_template_node.py", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "40d4e282aabbabbf5aff3ea6f966d3782852aa3204f33d65e216ede5e0fd66fe", + "chksum_sha256": "023e170fb1db59ec4d4acecee21befc2aa03199110368d4bd0682ad4adf84092", "format": 1 }, { - "name": "tests/integration/targets/project_manual", - "ftype": "dir", - "chksum_type": null, - "chksum_sha256": null, + "name": "plugins/modules/job_wait.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "7e7459abf351f6c172401eec4ba579dc8566f8a55fd022cc8eec9fa5a3399067", "format": 1 }, { - "name": "tests/integration/targets/project_manual/tasks", - "ftype": "dir", - "chksum_type": null, - "chksum_sha256": null, + "name": "plugins/modules/execution_environment.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "e00a63a1ddccfef4ad20725799ba67dbdfd8b9c720d9f04a92f05683bf7dc4e1", "format": 1 }, { - "name": "tests/integration/targets/project_manual/tasks/main.yml", + "name": "plugins/modules/instance.py", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "a1773c1365a408448e4facc5efdcc1b1e9f1f4a05e2eed59197dac00e9fa5105", + "chksum_sha256": "fb4467c11809837fee04ed77ddcf154414070a0b0458742e8d6dcb3be045a9f3", "format": 1 }, { - "name": "tests/integration/targets/project_manual/tasks/create_project_dir.yml", + "name": "plugins/modules/organization.py", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "562d2c4b88bbb2a3aa9ac76dbcb59e3cdf490e58f88c9971ff7e8b40bd4b3aca", + "chksum_sha256": "0c46808d096cba86747d9e9d69da660d44341c885fb966e4ffad8499b7d8d055", "format": 1 }, { - "name": "tests/integration/targets/workflow_approval", - "ftype": "dir", - "chksum_type": null, - "chksum_sha256": null, + "name": "plugins/modules/license.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "cf54b3a3c82fb705cb36a81cdab5e3cc25c8c8a798f3f43d22927c3164e97e69", "format": 1 }, { - "name": "tests/integration/targets/workflow_approval/tasks", - "ftype": "dir", - "chksum_type": null, - "chksum_sha256": null, + "name": "plugins/modules/group.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "7dc98af3cbdfc5ea0cf4cffe0c86c846837dfebe15c16939ed863b730cb05578", "format": 1 }, { - "name": "tests/integration/targets/workflow_approval/tasks/main.yml", + "name": "plugins/modules/subscriptions.py", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "d9891056c8e9ce1b3a2e78eebbc470979e4790bffafc3d5e4d22e90c7da4948d", + "chksum_sha256": "f497ab9ada8f89650422bf85deef386e32b774dfff9e1de07b387fba32d890a8", "format": 1 }, { - "name": "tests/integration/targets/label", - "ftype": "dir", - "chksum_type": null, - "chksum_sha256": null, + "name": "plugins/modules/workflow_node_wait.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "5b19778b005fbaa3e0a3abc645a6d6452bc0ad52e89fe04141d051f6ddafbb73", "format": 1 }, { - "name": "tests/integration/targets/label/tasks", - "ftype": "dir", - "chksum_type": null, - "chksum_sha256": null, + "name": "plugins/modules/label.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "f3bde75b41fd4c92037f759ae00e9ebd76f27c91ab54857f167715db1930b0a8", "format": 1 }, { - "name": "tests/integration/targets/label/tasks/main.yml", + "name": "plugins/modules/workflow_job_template.py", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "a54774f0ce66904eb1ab7c1389add53c60672d2b2d29b670034712f59e99d27d", + "chksum_sha256": "18fa1ae60ba7c409cd3baf67215ebf3b8d680e10a1402ca66f362166ab668cb4", "format": 1 }, { - "name": "tests/integration/targets/host", - "ftype": "dir", - "chksum_type": null, - "chksum_sha256": null, + "name": "plugins/modules/bulk_host_create.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "9398fec791845d1ddb304cd344d308127f89ce6ed5a8341f2c6047b4e2d22ed4", "format": 1 }, { - "name": "tests/integration/targets/host/tasks", - "ftype": "dir", - "chksum_type": null, - "chksum_sha256": null, + "name": "plugins/modules/project.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "d65ade1efc240c94eadc37d984cfec8094c655ed4d162243d170e947bad4b425", "format": 1 }, { - "name": "tests/integration/targets/host/tasks/main.yml", + "name": "plugins/modules/__init__.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "format": 1 + }, + { + "name": "plugins/modules/settings.py", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "9490898a74c074d553d8ac98dbc1d59bebc1d2a1f1899f28fd3165125ddfd44a", + "chksum_sha256": "c62170baca6d9ecf0be7a68d148daa75569e87a6ddffb92a39f3bc4ea08e9e0a", "format": 1 }, { - "name": "tests/integration/targets/inventory_source", - "ftype": "dir", - "chksum_type": null, - "chksum_sha256": null, + "name": "plugins/modules/project_update.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "952bedbca07dba7de277849b45eb258f51420ed8e814fa35acd47dc5e5f8f82f", "format": 1 }, { - "name": "tests/integration/targets/inventory_source/tasks", - "ftype": "dir", - "chksum_type": null, - "chksum_sha256": null, + "name": "plugins/modules/ad_hoc_command_wait.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "67bc716ec04dfc77cb751cda6013ee54fa0cd3ed3afabc5ba0d146cc9712c996", "format": 1 }, { - "name": "tests/integration/targets/inventory_source/tasks/main.yml", + "name": "plugins/modules/ad_hoc_command.py", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "55ef33f725875c00e6e31a928aff85257aacf5689c7f64a0924963c44b35c5af", + "chksum_sha256": "e0bc12df3832ad4c12eb7977c3dd69bee9eb0afdbd88755c06f01c2c42146869", "format": 1 }, { - "name": "tests/integration/targets/job_cancel", - "ftype": "dir", - "chksum_type": null, - "chksum_sha256": null, + "name": "plugins/modules/schedule.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "517c76a73dae1fadc7c47c0c8544ae00c6f88daff7a3e72c67aa9501265177ac", "format": 1 }, { - "name": "tests/integration/targets/job_cancel/tasks", - "ftype": "dir", - "chksum_type": null, - "chksum_sha256": null, + "name": "plugins/modules/inventory.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "1a430ae0b6371f884c14a9022552846f5174100ef02601a6ece649cb45a9e7e9", "format": 1 }, { - "name": "tests/integration/targets/job_cancel/tasks/main.yml", + "name": "plugins/modules/user.py", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "72e23ca4c467f6b23f23577597ad891613ec780c25fc00bb73bd3cd438783b2a", + "chksum_sha256": "cd3cc005d12434b3cac4238b3dfc0d2976f53f0c2e8c0dafc02953ad1393e377", "format": 1 }, { - "name": "tests/integration/targets/schedule", - "ftype": "dir", - "chksum_type": null, - "chksum_sha256": null, + "name": "plugins/modules/notification_template.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "638dbb75383b11b1227b2120424ee4f9f37861747c9aa5fca6df0665fe97fc86", "format": 1 }, { - "name": "tests/integration/targets/schedule/tasks", - "ftype": "dir", - "chksum_type": null, - "chksum_sha256": null, + "name": "plugins/modules/job_template.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "77a9774f8baae9a417aa3f7170f4866307ea3e7f29fbe93a00df98f33e8c99b0", "format": 1 }, { - "name": "tests/integration/targets/schedule/tasks/main.yml", + "name": "plugins/modules/bulk_job_launch.py", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "617b00f1a2de9e876ce63a471be371ac0e1622e8ab45062e9de8f5ea9ad8d4b2", + "chksum_sha256": "f19617e62f57b0a81fb9ebbf209583fbd9c74d3e2b85ca0102b2fd1b2bd77b81", "format": 1 }, { - "name": "tests/integration/targets/workflow_job_template", + "name": "plugins/module_utils", "ftype": "dir", "chksum_type": null, "chksum_sha256": null, "format": 1 }, { - "name": "tests/integration/targets/workflow_job_template/tasks", - "ftype": "dir", - "chksum_type": null, - "chksum_sha256": null, + "name": "plugins/module_utils/awxkit.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "8b2398e4e7893f203b26f6c85d510cc4c41a79c53e1937710807233e62e35f58", "format": 1 }, { - "name": "tests/integration/targets/workflow_job_template/tasks/main.yml", + "name": "plugins/module_utils/controller_api.py", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "2d872a591fbf23b975af6eacf660266ab8e0fb883af6b2bd33c058714b530b46", + "chksum_sha256": "424dfd5dbc14adedb59d26cfa67bbc46e1c5197859c473c9cdb288bb2ef26db9", "format": 1 }, { - "name": "tests/integration/targets/job_launch", + "name": "plugins/lookup", "ftype": "dir", "chksum_type": null, "chksum_sha256": null, "format": 1 }, { - "name": "tests/integration/targets/job_launch/tasks", - "ftype": "dir", - "chksum_type": null, - "chksum_sha256": null, + "name": "plugins/lookup/controller_api.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "5e79f19c9dee4fa0c3a88126a630fa6163249c332d73a44370f64836e22d4b27", "format": 1 }, { - "name": "tests/integration/targets/job_launch/tasks/main.yml", + "name": "plugins/lookup/schedule_rruleset.py", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "f4c564c9b92788fec1a7bdbe7ca53b2106ac7fee649912c64e366575ed3eb72a", + "chksum_sha256": "0141249f5cbe0651f96d3ba0a627a4d7e7376bd0e5b2b29e63ef44f9c243feb3", "format": 1 }, { - "name": "tests/integration/targets/instance", - "ftype": "dir", - "chksum_type": null, - "chksum_sha256": null, + "name": "plugins/lookup/schedule_rrule.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "c3ec7b8f134eca3a9f04156213b584792fc4e3397e3b9f82b5044e9ec662c7a2", "format": 1 }, { - "name": "tests/integration/targets/instance/tasks", + "name": "COPYING", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "7c50cd9b85e2b7eebaea2b5618b402862b01d5a66befff8e41401ef3f14e471a", + "format": 1 + }, + { + "name": "tests", "ftype": "dir", "chksum_type": null, "chksum_sha256": null, "format": 1 }, { - "name": "tests/integration/targets/instance/tasks/main.yml", + "name": "tests/config.yml", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "e9588dae873c72034ee98d463c80cb48c5a236b7e4d182786f3dee240ed89456", + "chksum_sha256": "4cb8bf065737689916cda6a2856fcfb8bc27f49224a4b2c2fde842e3b0e76fbb", "format": 1 }, { - "name": "tests/integration/targets/lookup_api_plugin", + "name": "tests/sanity", "ftype": "dir", "chksum_type": null, "chksum_sha256": null, "format": 1 }, { - "name": "tests/integration/targets/lookup_api_plugin/tasks", + "name": "tests/sanity/ignore-2.15.txt", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "f6dce33e05558d94ecc8ebacc8a5011e9defc1b197fcc13c4335868b6d6c4952", + "format": 1 + }, + { + "name": "tests/sanity/ignore-2.14.txt", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "05b621f6ff40c091ab1c07947c43d817ed37af7acfc0f8bef7b1453eb03b3aa7", + "format": 1 + }, + { + "name": "tests/sanity/ignore-2.16.txt", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "f6dce33e05558d94ecc8ebacc8a5011e9defc1b197fcc13c4335868b6d6c4952", + "format": 1 + }, + { + "name": "tests/integration", "ftype": "dir", "chksum_type": null, "chksum_sha256": null, "format": 1 }, { - "name": "tests/integration/targets/lookup_api_plugin/tasks/main.yml", - "ftype": "file", - "chksum_type": "sha256", - "chksum_sha256": "e47eaa102b38a0fc93f5a0fa3bf478a9f8b5fffce02737ff7099ae3dee1958ea", + "name": "tests/integration/targets", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, "format": 1 }, { - "name": "tests/integration/targets/lookup_rruleset", + "name": "tests/integration/targets/job_list", "ftype": "dir", "chksum_type": null, "chksum_sha256": null, "format": 1 }, { - "name": "tests/integration/targets/lookup_rruleset/tasks", + "name": "tests/integration/targets/job_list/tasks", "ftype": "dir", "chksum_type": null, "chksum_sha256": null, "format": 1 }, { - "name": "tests/integration/targets/lookup_rruleset/tasks/main.yml", + "name": "tests/integration/targets/job_list/tasks/main.yml", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "508564b7524b5f807dafd028ee5af7433bb5aace1e1396a3e1afd843f3b8f274", + "chksum_sha256": "668f25abe2486218893b6137f5b765301229f649ed3a779a6756496c14f42595", "format": 1 }, { - "name": "tests/integration/targets/project_update", + "name": "tests/integration/targets/credential_type", "ftype": "dir", "chksum_type": null, "chksum_sha256": null, "format": 1 }, { - "name": "tests/integration/targets/project_update/tasks", + "name": "tests/integration/targets/credential_type/tasks", "ftype": "dir", "chksum_type": null, "chksum_sha256": null, "format": 1 }, { - "name": "tests/integration/targets/project_update/tasks/main.yml", + "name": "tests/integration/targets/credential_type/tasks/main.yml", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "cfe9600b0f76c887c924cc051dbd978559f58ca45ad88c760fc44a1a4f1c5a08", + "chksum_sha256": "6d5026a4ca77513f73aa620ea14b74e0248c0dde7b5d75b318970154d7f4bf56", "format": 1 }, { - "name": "tests/integration/targets/job_list", + "name": "tests/integration/targets/job_wait", "ftype": "dir", "chksum_type": null, "chksum_sha256": null, "format": 1 }, { - "name": "tests/integration/targets/job_list/tasks", + "name": "tests/integration/targets/job_wait/tasks", "ftype": "dir", "chksum_type": null, "chksum_sha256": null, "format": 1 }, { - "name": "tests/integration/targets/job_list/tasks/main.yml", + "name": "tests/integration/targets/job_wait/tasks/main.yml", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "0f71862d199385973c479a2ce6a2c9eb060c80cfca19b82026a19ec60308f1b3", + "chksum_sha256": "76cf3f8c1cfac81eaaf0f5b76494fc3b7605a15eeb73f6fc83efbbe28a4073b3", "format": 1 }, { - "name": "tests/integration/targets/demo_data", + "name": "tests/integration/targets/workflow_approval", "ftype": "dir", "chksum_type": null, "chksum_sha256": null, "format": 1 }, { - "name": "tests/integration/targets/demo_data/tasks", + "name": "tests/integration/targets/workflow_approval/tasks", "ftype": "dir", "chksum_type": null, "chksum_sha256": null, "format": 1 }, { - "name": "tests/integration/targets/demo_data/tasks/main.yml", + "name": "tests/integration/targets/workflow_approval/tasks/main.yml", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "617098185b6890b6c01d85830deb32bda385ca2499ea0c6f5f8bf44f1bedae28", + "chksum_sha256": "2ccc6f9f0aafef620896c56a32e1dd07f2f83f61c6b8c4d4bbaa6a220495b91d", "format": 1 }, { - "name": "tests/integration/targets/job_template", + "name": "tests/integration/targets/application", "ftype": "dir", "chksum_type": null, "chksum_sha256": null, "format": 1 }, { - "name": "tests/integration/targets/job_template/tasks", + "name": "tests/integration/targets/application/tasks", "ftype": "dir", "chksum_type": null, "chksum_sha256": null, "format": 1 }, { - "name": "tests/integration/targets/job_template/tasks/main.yml", + "name": "tests/integration/targets/application/tasks/main.yml", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "121573eb6c0556945387c8c8eb533dda8da637c3470c6b53a4cdd7d85f1b58d6", + "chksum_sha256": "42675b283f15715cccd1bba3f3d6be8d9d84e1bcf7c1e06e33a281ab4eb5c64f", "format": 1 }, { - "name": "tests/integration/targets/workflow_launch", + "name": "tests/integration/targets/instance_group", "ftype": "dir", "chksum_type": null, "chksum_sha256": null, "format": 1 }, { - "name": "tests/integration/targets/workflow_launch/tasks", + "name": "tests/integration/targets/instance_group/tasks", "ftype": "dir", "chksum_type": null, "chksum_sha256": null, "format": 1 }, { - "name": "tests/integration/targets/workflow_launch/tasks/main.yml", + "name": "tests/integration/targets/instance_group/tasks/main.yml", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "dd5c0022ff17e2eebb52303e1f5132eff0cdf35e737f122b0573f928cdd7ad03", + "chksum_sha256": "5d38320dd4563f17fffd12b510a91dd26e40bfd5f408f75f96b930a38fcfd135", "format": 1 }, { - "name": "tests/integration/targets/team", + "name": "tests/integration/targets/settings", "ftype": "dir", "chksum_type": null, "chksum_sha256": null, "format": 1 }, { - "name": "tests/integration/targets/team/tasks", + "name": "tests/integration/targets/settings/tasks", "ftype": "dir", "chksum_type": null, "chksum_sha256": null, "format": 1 }, { - "name": "tests/integration/targets/team/tasks/main.yml", + "name": "tests/integration/targets/settings/tasks/main.yml", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "ea5b32cc64a4b1553bc5b254b09ef61187911e6364bb7a4d3c3159233182bdc7", + "chksum_sha256": "93262aa7be1aec73bcd5e36b1fadd663bed21eab8863f797dd17f20de80db7ba", "format": 1 }, { - "name": "tests/integration/targets/notification_template", + "name": "tests/integration/targets/project", "ftype": "dir", "chksum_type": null, "chksum_sha256": null, "format": 1 }, { - "name": "tests/integration/targets/notification_template/tasks", + "name": "tests/integration/targets/project/tasks", "ftype": "dir", "chksum_type": null, "chksum_sha256": null, "format": 1 }, { - "name": "tests/integration/targets/notification_template/tasks/main.yml", + "name": "tests/integration/targets/project/tasks/main.yml", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "fe7f0d86e9bde61f8b708757f0b0627f5ace6b7cbdaf7103f8b247289ae5a295", + "chksum_sha256": "c2f8af4ca361cc63c465d7b0895ef2ad7a3741f36e30133b5c33696ba2cf7474", "format": 1 }, { - "name": "tests/integration/targets/application", + "name": "tests/integration/targets/inventory_source", "ftype": "dir", "chksum_type": null, "chksum_sha256": null, "format": 1 }, { - "name": "tests/integration/targets/application/tasks", + "name": "tests/integration/targets/inventory_source/tasks", "ftype": "dir", "chksum_type": null, "chksum_sha256": null, "format": 1 }, { - "name": "tests/integration/targets/application/tasks/main.yml", + "name": "tests/integration/targets/inventory_source/tasks/main.yml", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "d95ffd0fd9a79fd5c9bd95841d6e88f0ad9a8d2f4376a1d66a3432a48cc8e445", + "chksum_sha256": "7e581c4634196ff64cee14278c55b2292534605006e5a20edadcfe17e778caa9", "format": 1 }, { @@ -669,98 +725,91 @@ "name": "tests/integration/targets/group/tasks/main.yml", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "7444fbbb58dc4c93953187b843cbdc3198427571a1fd49f36bc8d71d23b70479", + "chksum_sha256": "fe499cf6b248289df031a4d32328dbe45184f4fe7d92423522b1705715010904", "format": 1 }, { - "name": "tests/integration/targets/schedule_rrule", + "name": "tests/integration/targets/demo_data", "ftype": "dir", "chksum_type": null, "chksum_sha256": null, "format": 1 }, { - "name": "tests/integration/targets/schedule_rrule/tasks", + "name": "tests/integration/targets/demo_data/tasks", "ftype": "dir", "chksum_type": null, "chksum_sha256": null, "format": 1 }, { - "name": "tests/integration/targets/schedule_rrule/tasks/main.yml", + "name": "tests/integration/targets/demo_data/tasks/main.yml", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "2d3b202620a305fcf477836f22ce7e52a195b0f43b3c854ffec5763d6411b26b", + "chksum_sha256": "619e28fefe26c4854b15b8c739693b1da85f69b1cf9792c84f40db544dda5f8b", "format": 1 }, { - "name": "tests/integration/targets/user", + "name": "tests/integration/targets/credential", "ftype": "dir", "chksum_type": null, "chksum_sha256": null, "format": 1 }, { - "name": "tests/integration/targets/user/tasks", + "name": "tests/integration/targets/credential/tasks", "ftype": "dir", "chksum_type": null, "chksum_sha256": null, "format": 1 }, { - "name": "tests/integration/targets/user/tasks/main.yml", + "name": "tests/integration/targets/credential/tasks/main.yml", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "88f742f66ccac2fed93222a997e02129c43e9dd863ce8b9a2fd8e07dd6973916", + "chksum_sha256": "113f6f4c35885fe2a1ebca33523c9a6284a36ae470963e41a12909e444e0281d", "format": 1 }, { - "name": "tests/integration/targets/settings", + "name": "tests/integration/targets/schedule", "ftype": "dir", "chksum_type": null, "chksum_sha256": null, "format": 1 }, { - "name": "tests/integration/targets/settings/tasks", + "name": "tests/integration/targets/schedule/tasks", "ftype": "dir", "chksum_type": null, "chksum_sha256": null, "format": 1 }, { - "name": "tests/integration/targets/settings/tasks/main.yml", + "name": "tests/integration/targets/schedule/tasks/main.yml", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "1d02d2e1a163b170fa15d54b37ec7da22509d45f4cc194583ec1a1c5d5682b16", + "chksum_sha256": "7419baf00681606be0a250140ace5f7d07804f8b650ecb5f0937147064e307f0", "format": 1 }, { - "name": "tests/integration/targets/import", + "name": "tests/integration/targets/organization", "ftype": "dir", "chksum_type": null, "chksum_sha256": null, "format": 1 }, { - "name": "tests/integration/targets/import/tasks", + "name": "tests/integration/targets/organization/tasks", "ftype": "dir", "chksum_type": null, "chksum_sha256": null, "format": 1 }, { - "name": "tests/integration/targets/import/tasks/main.yml", - "ftype": "file", - "chksum_type": "sha256", - "chksum_sha256": "59c0ace95e680d9874fe15c76889c1b4beb38d2d3c66a11499581b0f328ec25a", - "format": 1 - }, - { - "name": "tests/integration/targets/import/aliases", + "name": "tests/integration/targets/organization/tasks/main.yml", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "52e1315ef042495cdf2b0ce22d8ba47f726dce15b968e301a795be1f69045f20", + "chksum_sha256": "6e18cbff8118f617a9b594f630739356922030c2d802a0eb92df0ddbe4f1e1fe", "format": 1 }, { @@ -781,140 +830,140 @@ "name": "tests/integration/targets/inventory_source_update/tasks/main.yml", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "c9da57401129b24c4c6b2a54acd923a5e8f82a884ad94b23dbf1cf4dfad847cb", + "chksum_sha256": "06a737371470d7a996e2eb092d820fe536dac73f3abcec56acf002da62fe05d9", "format": 1 }, { - "name": "tests/integration/targets/job_wait", + "name": "tests/integration/targets/lookup_api_plugin", "ftype": "dir", "chksum_type": null, "chksum_sha256": null, "format": 1 }, { - "name": "tests/integration/targets/job_wait/tasks", + "name": "tests/integration/targets/lookup_api_plugin/tasks", "ftype": "dir", "chksum_type": null, "chksum_sha256": null, "format": 1 }, { - "name": "tests/integration/targets/job_wait/tasks/main.yml", + "name": "tests/integration/targets/lookup_api_plugin/tasks/main.yml", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "d7dd7fb1b9b81268a3d35a9df424fc977df5463bdef0c00b34bda6fab98682c9", + "chksum_sha256": "d2206729f900ec7f71e77edf782eff6e68160ccb0124fb2bf22f0f371abb9271", "format": 1 }, { - "name": "tests/integration/targets/role", + "name": "tests/integration/targets/import", "ftype": "dir", "chksum_type": null, "chksum_sha256": null, "format": 1 }, { - "name": "tests/integration/targets/role/tasks", + "name": "tests/integration/targets/import/aliases", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "52e1315ef042495cdf2b0ce22d8ba47f726dce15b968e301a795be1f69045f20", + "format": 1 + }, + { + "name": "tests/integration/targets/import/tasks", "ftype": "dir", "chksum_type": null, "chksum_sha256": null, "format": 1 }, { - "name": "tests/integration/targets/role/tasks/main.yml", + "name": "tests/integration/targets/import/tasks/main.yml", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "b5f1f2624634486869b5e587c44fb44d0849b926364bd14766a8921cbcfe3674", + "chksum_sha256": "59c0ace95e680d9874fe15c76889c1b4beb38d2d3c66a11499581b0f328ec25a", "format": 1 }, { - "name": "tests/integration/targets/organization", + "name": "tests/integration/targets/job_cancel", "ftype": "dir", "chksum_type": null, "chksum_sha256": null, "format": 1 }, { - "name": "tests/integration/targets/organization/tasks", + "name": "tests/integration/targets/job_cancel/tasks", "ftype": "dir", "chksum_type": null, "chksum_sha256": null, "format": 1 }, { - "name": "tests/integration/targets/organization/tasks/main.yml", + "name": "tests/integration/targets/job_cancel/tasks/main.yml", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "73853c4fd1dde833e599df55d8636e3cabac2e1139eac0785c75a9b7b6fac00f", + "chksum_sha256": "0c8b9d511b1ce96b7d5a359b1043ffc18004c5d3c23052b82423512b46ecd7b4", "format": 1 }, { - "name": "tests/integration/targets/credential", + "name": "tests/integration/targets/ad_hoc_command", "ftype": "dir", "chksum_type": null, "chksum_sha256": null, "format": 1 }, { - "name": "tests/integration/targets/credential/tasks", + "name": "tests/integration/targets/ad_hoc_command/tasks", "ftype": "dir", "chksum_type": null, "chksum_sha256": null, "format": 1 }, { - "name": "tests/integration/targets/credential/tasks/main.yml", + "name": "tests/integration/targets/ad_hoc_command/tasks/main.yml", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "df1406f279fb76a35c21f2e178b20f6268f30b15305200d7bff4f70d051d3284", + "chksum_sha256": "87ec6c33a66b6dd969c6ed08693a02a51fc43c21b3980a81a5edd29d644050aa", "format": 1 }, { - "name": "tests/integration/targets/export", + "name": "tests/integration/targets/inventory", "ftype": "dir", "chksum_type": null, "chksum_sha256": null, "format": 1 }, { - "name": "tests/integration/targets/export/tasks", + "name": "tests/integration/targets/inventory/tasks", "ftype": "dir", "chksum_type": null, "chksum_sha256": null, "format": 1 }, { - "name": "tests/integration/targets/export/tasks/main.yml", - "ftype": "file", - "chksum_type": "sha256", - "chksum_sha256": "e66e796b995b7c9ae612e00b393ccd75d9747d2d94ea3fbbaf90832e5b3e9e3f", - "format": 1 - }, - { - "name": "tests/integration/targets/export/aliases", + "name": "tests/integration/targets/inventory/tasks/main.yml", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "52e1315ef042495cdf2b0ce22d8ba47f726dce15b968e301a795be1f69045f20", + "chksum_sha256": "21f6d6fa5e76c8578949fd204edef4cdb77b5bff310b681bcd75ac1873a67f18", "format": 1 }, { - "name": "tests/integration/targets/token", + "name": "tests/integration/targets/role", "ftype": "dir", "chksum_type": null, "chksum_sha256": null, "format": 1 }, { - "name": "tests/integration/targets/token/tasks", + "name": "tests/integration/targets/role/tasks", "ftype": "dir", "chksum_type": null, "chksum_sha256": null, "format": 1 }, { - "name": "tests/integration/targets/token/tasks/main.yml", + "name": "tests/integration/targets/role/tasks/main.yml", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "606e16dcd72ab4a0a6c26aedf8830e1de844266e7fa54254c93ed7e307c950d7", + "chksum_sha256": "24e699037b51baceceb9f18e1e43464608766127f5eeb4df1fed5991a35aea7f", "format": 1 }, { @@ -935,28 +984,28 @@ "name": "tests/integration/targets/execution_environment/tasks/main.yml", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "f51f07de38999eff7fbacbd72929a622afc5b9fb01f2acd44cae288978948c64", + "chksum_sha256": "950122a04ad3b7406fa9a140e02ab190a0f7f53dfec791fdcb1fb2781ebae54d", "format": 1 }, { - "name": "tests/integration/targets/credential_input_source", + "name": "tests/integration/targets/lookup_rruleset", "ftype": "dir", "chksum_type": null, "chksum_sha256": null, "format": 1 }, { - "name": "tests/integration/targets/credential_input_source/tasks", + "name": "tests/integration/targets/lookup_rruleset/tasks", "ftype": "dir", "chksum_type": null, "chksum_sha256": null, "format": 1 }, { - "name": "tests/integration/targets/credential_input_source/tasks/main.yml", + "name": "tests/integration/targets/lookup_rruleset/tasks/main.yml", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "ccc6e4527f9019c28f32cdb7b223d1a4445f2f505ef448e8a2e255c8981bd927", + "chksum_sha256": "30edd075f50be0fbf9c57c9d7db10ae7086749d78817e8a24b680301ba339cb5", "format": 1 }, { @@ -977,679 +1026,686 @@ "name": "tests/integration/targets/ad_hoc_command_wait/tasks/main.yml", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "ce9d9c82599c3673f4a0d7da0b7af4437bb32689b5ff607266f0c875a7b7f2b7", + "chksum_sha256": "1e5ced853c1d15d2db163bd5dbb56b1ca419363327dc32bd6a1448560e194c2f", "format": 1 }, { - "name": "tests/config.yml", - "ftype": "file", - "chksum_type": "sha256", - "chksum_sha256": "4cb8bf065737689916cda6a2856fcfb8bc27f49224a4b2c2fde842e3b0e76fbb", + "name": "tests/integration/targets/job_template", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, "format": 1 }, { - "name": "tests/sanity", + "name": "tests/integration/targets/job_template/tasks", "ftype": "dir", "chksum_type": null, "chksum_sha256": null, "format": 1 }, { - "name": "tests/sanity/ignore-2.15.txt", + "name": "tests/integration/targets/job_template/tasks/main.yml", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "05b621f6ff40c091ab1c07947c43d817ed37af7acfc0f8bef7b1453eb03b3aa7", + "chksum_sha256": "b1232f2cc28b0ff4ff742c2c5847a649d3d9fcc716d3611a938b277088a0e30b", "format": 1 }, { - "name": "tests/sanity/ignore-2.14.txt", - "ftype": "file", - "chksum_type": "sha256", - "chksum_sha256": "05b621f6ff40c091ab1c07947c43d817ed37af7acfc0f8bef7b1453eb03b3aa7", + "name": "tests/integration/targets/token", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, "format": 1 }, { - "name": "meta", + "name": "tests/integration/targets/token/tasks", "ftype": "dir", "chksum_type": null, "chksum_sha256": null, "format": 1 }, { - "name": "meta/runtime.yml", + "name": "tests/integration/targets/token/tasks/main.yml", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "8f31c7db3b2274e381a8e6c9dff564f47815ab49ec28b472ed1ec678a331ca06", + "chksum_sha256": "e7245f5039915d720baba505b4ab74421c7db138bb45b50917942f3eb05799af", "format": 1 }, { - "name": "test", + "name": "tests/integration/targets/export", "ftype": "dir", "chksum_type": null, "chksum_sha256": null, "format": 1 }, { - "name": "test/awx", - "ftype": "dir", - "chksum_type": null, - "chksum_sha256": null, + "name": "tests/integration/targets/export/aliases", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "52e1315ef042495cdf2b0ce22d8ba47f726dce15b968e301a795be1f69045f20", "format": 1 }, { - "name": "test/awx/test_token.py", - "ftype": "file", - "chksum_type": "sha256", - "chksum_sha256": "118145cdd5f6a03df7a7a608d5f9e510236b2a54f9bcd456f4294ba69f0f4fad", + "name": "tests/integration/targets/export/tasks", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, "format": 1 }, { - "name": "test/awx/test_notification_template.py", + "name": "tests/integration/targets/export/tasks/main.yml", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "f40d5b65fbc78d12570f37799c8e240cfb90d9948421d3db82af6427fd14854f", + "chksum_sha256": "e66e796b995b7c9ae612e00b393ccd75d9747d2d94ea3fbbaf90832e5b3e9e3f", "format": 1 }, { - "name": "test/awx/test_ad_hoc_wait.py", - "ftype": "file", - "chksum_type": "sha256", - "chksum_sha256": "daed2a74d3f64fd0300255050dc8c732158db401323f44da66ccb4bf84b59633", + "name": "tests/integration/targets/host", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, "format": 1 }, { - "name": "test/awx/test_instance_group.py", - "ftype": "file", - "chksum_type": "sha256", - "chksum_sha256": "9ce22bf5e6baa63ab096c9377478f8a3af33624def33e52753342e435924e573", + "name": "tests/integration/targets/host/tasks", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, "format": 1 }, { - "name": "test/awx/test_application.py", + "name": "tests/integration/targets/host/tasks/main.yml", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "a106d5fbffbe1eaec36d8247979ca637ee733a29abf94d955c48be8d2fd16842", + "chksum_sha256": "bc0849abd5d11fe3fa3039ed0ae93d4e812d915e66c47488f91cc4309b2d7d77", "format": 1 }, { - "name": "test/awx/test_organization.py", - "ftype": "file", - "chksum_type": "sha256", - "chksum_sha256": "90dacaf268600864f01bdfdb0eb34f0225a605320b5af73754cbc229610e5d24", + "name": "tests/integration/targets/schedule_rrule", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, "format": 1 }, { - "name": "test/awx/test_completeness.py", - "ftype": "file", - "chksum_type": "sha256", - "chksum_sha256": "9bdbafb6fc36ac375a5674bebe7a603285ff98b891f66e41412518bd6cb4f72a", + "name": "tests/integration/targets/schedule_rrule/tasks", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, "format": 1 }, { - "name": "test/awx/test_user.py", + "name": "tests/integration/targets/schedule_rrule/tasks/main.yml", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "f9520b058e16e4e4800d3a5f70cd28650a365fa357afa1d41a8c63bf3354027e", + "chksum_sha256": "c8732015804e039989986f1ebfc82da8d426c7d8ab21a7602b101ed16d4c33e8", "format": 1 }, { - "name": "test/awx/test_credential_type.py", - "ftype": "file", - "chksum_type": "sha256", - "chksum_sha256": "1fe388a0c19f08006c7718766d5faa79540dd3b14547ced43b5a237a2c2fd877", + "name": "tests/integration/targets/label", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, "format": 1 }, { - "name": "test/awx/test_module_utils.py", - "ftype": "file", - "chksum_type": "sha256", - "chksum_sha256": "da19747889f28cba3f49836ef64363a010c6cb78650456183efba297d71f0def", + "name": "tests/integration/targets/label/tasks", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, "format": 1 }, { - "name": "test/awx/test_bulk.py", + "name": "tests/integration/targets/label/tasks/main.yml", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "194490a94b5d7e3444115a0ead711061cce2ac737ecbcdb786c3d673e919f837", + "chksum_sha256": "19b0b776638ca5722ddc8fc85fc90fba2639abecbd5dc5bbc6d7ee3c53b5ff16", "format": 1 }, { - "name": "test/awx/test_job_template.py", - "ftype": "file", - "chksum_type": "sha256", - "chksum_sha256": "75528184cbc1e92aafc05360990d0280cf64f3bb7049120090ef25a3feb114ac", + "name": "tests/integration/targets/credential_input_source", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, "format": 1 }, { - "name": "test/awx/test_group.py", - "ftype": "file", - "chksum_type": "sha256", - "chksum_sha256": "1ecf188e82d4c848de64c8f7fd7af2d4adb6887c6a448771ff51bb43c4fa8128", + "name": "tests/integration/targets/credential_input_source/tasks", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, "format": 1 }, { - "name": "test/awx/test_role.py", + "name": "tests/integration/targets/credential_input_source/tasks/main.yml", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "8f0340f77fd1465cf6e267b301e44ae86c5238b05aa89bd7fff145726a83ebb4", + "chksum_sha256": "83dae6f438515cce80bf4b25d24d472de42feafe46b9f8faacaa7cdf18c1eb16", "format": 1 }, { - "name": "test/awx/test_inventory.py", - "ftype": "file", - "chksum_type": "sha256", - "chksum_sha256": "70eac0cf78806e37406137fcfb97e5a249fd6b091b1f18e812278573049a4111", + "name": "tests/integration/targets/project_update", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, "format": 1 }, { - "name": "test/awx/test_project.py", - "ftype": "file", - "chksum_type": "sha256", - "chksum_sha256": "fbb93e524df51b788f12746ffb52bf5105f67b3ae6b89403bed51ed1f2da9c12", + "name": "tests/integration/targets/project_update/tasks", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, "format": 1 }, { - "name": "test/awx/test_team.py", + "name": "tests/integration/targets/project_update/tasks/main.yml", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "cbbdbdb3be0b0d80dcfcf337ed0095774cf73ef0e937d3e8dc5abab21739db5d", + "chksum_sha256": "6fd0d5b30de4d13f2d2e5fb7497c0fbbc13fa92a31fc43d4a21b19d3f53249ce", "format": 1 }, { - "name": "test/awx/test_label.py", - "ftype": "file", - "chksum_type": "sha256", - "chksum_sha256": "cd957d0b0cab6dd51539baf3fb27b659b91a8e57b20aae4c5cce7eaec9cec494", + "name": "tests/integration/targets/module_utils", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, "format": 1 }, { - "name": "test/awx/test_workflow_job_template_node.py", - "ftype": "file", - "chksum_type": "sha256", - "chksum_sha256": "0806356bfd91b28153baa63ca8cbf8f7da1125dd5150e38e73aa37c65e236f6b", + "name": "tests/integration/targets/module_utils/tasks", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, "format": 1 }, { - "name": "test/awx/conftest.py", + "name": "tests/integration/targets/module_utils/tasks/test_named_reference.yml", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "09e6eaaa58debfcd70ebd6fd92baafd2fea879b59c60f4cfa8c866ae04977a4d", + "chksum_sha256": "0e1babbc9e57e06629675f6d2b0d4fe891880b177a572d355391753e670c5156", "format": 1 }, { - "name": "test/awx/test_inventory_source.py", + "name": "tests/integration/targets/module_utils/tasks/main.yml", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "a14f69db1bf6cec594e64c2963b415560b78eac1f9cbe7d4c09586b494e11bde", + "chksum_sha256": "2fb7f33e9695e68ec87d2b9bc52ad2196c2d542bccb7d5b185ebc8e54a8cc237", "format": 1 }, { - "name": "test/awx/test_credential_input_source.py", - "ftype": "file", - "chksum_type": "sha256", - "chksum_sha256": "9637a418c0b0e59261ec0d1c206ff2d3574a41a8a169068bbf74588e3a4214b2", + "name": "tests/integration/targets/user", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, "format": 1 }, { - "name": "test/awx/test_workflow_job_template.py", - "ftype": "file", - "chksum_type": "sha256", - "chksum_sha256": "16c8ebc74606940f6ee1f51a191f22b497c176a46e770e886bbf94bdf0c25842", + "name": "tests/integration/targets/user/tasks", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, "format": 1 }, { - "name": "test/awx/test_schedule.py", + "name": "tests/integration/targets/user/tasks/main.yml", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "dadfd1c19c4c828dd84128ca484b837c6a904a09e92bcee12cb7cda408562c81", + "chksum_sha256": "76733e6cbaba31ccf3510bea4ec1166c93195d5ef385035bb773fba854db3659", "format": 1 }, { - "name": "test/awx/test_credential.py", - "ftype": "file", - "chksum_type": "sha256", - "chksum_sha256": "1f8348d6f37932997c7971beb8c5f92cf649523e3d3c5d5e859846460d7d1e8d", + "name": "tests/integration/targets/notification_template", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, "format": 1 }, { - "name": "test/awx/test_settings.py", - "ftype": "file", - "chksum_type": "sha256", - "chksum_sha256": "436c13933936e7b80dd26c61ea1dbf492c13974f2922f1543c4fe6e6b0fab0dd", + "name": "tests/integration/targets/notification_template/tasks", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, "format": 1 }, { - "name": "test/awx/test_job.py", + "name": "tests/integration/targets/notification_template/tasks/main.yml", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "76ba45e14438425f7511d196613928d64253e1912a45b71ea842b1cb2c3ca335", + "chksum_sha256": "9d4d3cef57b6e950d9af710bb519050823ef67fd6aa1c6b60c6f2fc33387dc67", "format": 1 }, { - "name": "plugins", + "name": "tests/integration/targets/ad_hoc_command_cancel", "ftype": "dir", "chksum_type": null, "chksum_sha256": null, "format": 1 }, { - "name": "plugins/module_utils", + "name": "tests/integration/targets/ad_hoc_command_cancel/tasks", "ftype": "dir", "chksum_type": null, "chksum_sha256": null, "format": 1 }, { - "name": "plugins/module_utils/tower_legacy.py", + "name": "tests/integration/targets/ad_hoc_command_cancel/tasks/main.yml", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "442535992f6564ac689645ff6e880848762eafc0d93a3255cbe5bedec5eefd58", + "chksum_sha256": "1d276bf3b4ada55707a1e8a00284ab15a5c8f9a6bbe0a49f7d03160cda21d3c1", "format": 1 }, { - "name": "plugins/module_utils/controller_api.py", - "ftype": "file", - "chksum_type": "sha256", - "chksum_sha256": "999dfa9d68668fa30823eb837ad3703d2be624413b3078441a040468b89dd16f", + "name": "tests/integration/targets/instance", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, "format": 1 }, { - "name": "plugins/module_utils/awxkit.py", + "name": "tests/integration/targets/instance/tasks", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, + "format": 1 + }, + { + "name": "tests/integration/targets/instance/tasks/main.yml", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "8b2398e4e7893f203b26f6c85d510cc4c41a79c53e1937710807233e62e35f58", + "chksum_sha256": "ccd821e64446cc63715163fa16014f10e76c66d58be119bfe1dc1a3b11637c97", "format": 1 }, { - "name": "plugins/doc_fragments", + "name": "tests/integration/targets/team", "ftype": "dir", "chksum_type": null, "chksum_sha256": null, "format": 1 }, { - "name": "plugins/doc_fragments/auth.py", - "ftype": "file", - "chksum_type": "sha256", - "chksum_sha256": "08510309125b9276dca6553a3c77436c0a225c250eea33d54be356a68a06a5f3", + "name": "tests/integration/targets/team/tasks", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, "format": 1 }, { - "name": "plugins/doc_fragments/auth_plugin.py", + "name": "tests/integration/targets/team/tasks/main.yml", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "80afe672d9386df036747cda82e54091e9717cdecfeab47b8567502b2ac3fbd1", + "chksum_sha256": "f59433918d9495fd5a3b296a0a70963f539571e742e3c790e5d88912eec8e2ef", "format": 1 }, { - "name": "plugins/doc_fragments/auth_legacy.py", - "ftype": "file", - "chksum_type": "sha256", - "chksum_sha256": "c2f10b81ecb89088c7c295430d4a71de26e3700b26e8344cdc7950908a738fd3", + "name": "tests/integration/targets/workflow_job_template", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, "format": 1 }, { - "name": "plugins/inventory", + "name": "tests/integration/targets/workflow_job_template/tasks", "ftype": "dir", "chksum_type": null, "chksum_sha256": null, "format": 1 }, { - "name": "plugins/inventory/controller.py", + "name": "tests/integration/targets/workflow_job_template/tasks/main.yml", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "29e7cd36a2b18ee616e31cbbec6a6e103f3f18ebe13f2bb87167b915163ca4bf", + "chksum_sha256": "872f3cd4d0d7656ddf7f9239c1139351af6341e5e40e0ac2981ed152fff26a5b", "format": 1 }, { - "name": "plugins/modules", + "name": "tests/integration/targets/job_launch", "ftype": "dir", "chksum_type": null, "chksum_sha256": null, "format": 1 }, { - "name": "plugins/modules/project_update.py", - "ftype": "file", - "chksum_type": "sha256", - "chksum_sha256": "2b06aacf9e51faa8b51fe770d3663e4e6e6d9e382769edf6883cd04414d3cd8c", + "name": "tests/integration/targets/job_launch/tasks", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, "format": 1 }, { - "name": "plugins/modules/project.py", + "name": "tests/integration/targets/job_launch/tasks/main.yml", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "c93ddb644de67226bd22d27d5490613c447b80fcf561927445af157e8c796674", + "chksum_sha256": "fbb658e8212fcb120200db84212f6b469fff700c1fd233b486ec82a816572323", "format": 1 }, { - "name": "plugins/modules/workflow_job_template.py", - "ftype": "file", - "chksum_type": "sha256", - "chksum_sha256": "c8a3498557011c1fa118006166aabb13a9ddba4eaca5a50d5469214b49d8153b", + "name": "tests/integration/targets/bulk_host_delete", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, "format": 1 }, { - "name": "plugins/modules/credential_input_source.py", - "ftype": "file", - "chksum_type": "sha256", - "chksum_sha256": "d90dd76b3b2a42ceaf423d05755c4c61bc565370f7905aecf9a516172761b60b", + "name": "tests/integration/targets/bulk_host_delete/tasks", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, "format": 1 }, { - "name": "plugins/modules/credential.py", + "name": "tests/integration/targets/bulk_host_delete/tasks/main.yml", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "6a1882a9893a096ae4f0a89028e4447719daa33051eaf54919e88d07a3df8ef5", + "chksum_sha256": "2a55e10b05593a8ad6f5e2325c3d51f76b5ef040a81990dc3c7a4adaeebcf545", "format": 1 }, { - "name": "plugins/modules/job_wait.py", - "ftype": "file", - "chksum_type": "sha256", - "chksum_sha256": "7e7459abf351f6c172401eec4ba579dc8566f8a55fd022cc8eec9fa5a3399067", + "name": "tests/integration/targets/bulk_host_create", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, "format": 1 }, { - "name": "plugins/modules/ad_hoc_command.py", - "ftype": "file", - "chksum_type": "sha256", - "chksum_sha256": "4dcafb33a0487b4200a6abcf3283dd55335de9102a2740c93e24b0e9e7ef224d", + "name": "tests/integration/targets/bulk_host_create/tasks", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, "format": 1 }, { - "name": "plugins/modules/credential_type.py", + "name": "tests/integration/targets/bulk_host_create/tasks/main.yml", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "c56a3cf4eddc284b0c83e55a3f58d19a9d315a68abf513d232c3fe5b81ec85f3", + "chksum_sha256": "39be6c097c03152ec1873732d8e09e34639464acad4f9d42ef3e13b6f87d47d1", "format": 1 }, { - "name": "plugins/modules/workflow_launch.py", - "ftype": "file", - "chksum_type": "sha256", - "chksum_sha256": "91eabbcdfed14efb72a6b02db83cd4f92c811a77e55119e9b0fefb6453eee953", + "name": "tests/integration/targets/bulk_job_launch", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, "format": 1 }, { - "name": "plugins/modules/workflow_node_wait.py", - "ftype": "file", - "chksum_type": "sha256", - "chksum_sha256": "5b19778b005fbaa3e0a3abc645a6d6452bc0ad52e89fe04141d051f6ddafbb73", + "name": "tests/integration/targets/bulk_job_launch/tasks", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, "format": 1 }, { - "name": "plugins/modules/ad_hoc_command_wait.py", + "name": "tests/integration/targets/bulk_job_launch/tasks/main.yml", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "67bc716ec04dfc77cb751cda6013ee54fa0cd3ed3afabc5ba0d146cc9712c996", + "chksum_sha256": "bbb7480d55d9d2261e22d0c55de3a0bada691608a6542385b234633f0493be75", "format": 1 }, { - "name": "plugins/modules/subscriptions.py", - "ftype": "file", - "chksum_type": "sha256", - "chksum_sha256": "f497ab9ada8f89650422bf85deef386e32b774dfff9e1de07b387fba32d890a8", + "name": "tests/integration/targets/workflow_launch", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, "format": 1 }, { - "name": "plugins/modules/role.py", - "ftype": "file", - "chksum_type": "sha256", - "chksum_sha256": "26f6dfe334c409b0ead538ff1c9a1c20c88d673db374fabdd5b3cfaeeb30e70e", + "name": "tests/integration/targets/workflow_launch/tasks", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, "format": 1 }, { - "name": "plugins/modules/inventory_source.py", + "name": "tests/integration/targets/workflow_launch/tasks/main.yml", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "b939811acd5dd6100b7436633fd3d537d4c44c9006bf29bfa51574db744c0838", + "chksum_sha256": "4ceb05ce9ac229472ee12b492754aeacba13b8060a8e973467aba8a9d3d793b6", "format": 1 }, { - "name": "plugins/modules/application.py", + "name": "requirements.txt", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "a932d66c23f578fc62a733ac466f5732d0ed2d2192252b108de21c4da219880c", + "chksum_sha256": "2eb11923e1347afc5075a7871e206a8f15a68471c90012f7386e9db0875e70bf", "format": 1 }, { - "name": "plugins/modules/user.py", - "ftype": "file", - "chksum_type": "sha256", - "chksum_sha256": "5bca255128e1f376a15622a9dfbf6a469c23f6d9528a5df6e318a503402214e6", + "name": "images", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, "format": 1 }, { - "name": "plugins/modules/host.py", + "name": "images/completeness_test_output.png", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "e42e558d5f91555f6f4f32b186d32d2920aad6126b164448c6258a1ee9f847ef", + "chksum_sha256": "6367684c4b5edd3e1e8fdcb9270d68ca54040d5d17108734f3d3a2b9df5878ba", "format": 1 }, { - "name": "plugins/modules/job_template.py", - "ftype": "file", - "chksum_type": "sha256", - "chksum_sha256": "8111cffd44f026c9997ab2315ea3a2fd984754caa5953a89ed6da9d9a257bcd1", + "name": "test", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, "format": 1 }, { - "name": "plugins/modules/organization.py", - "ftype": "file", - "chksum_type": "sha256", - "chksum_sha256": "020576aef74ec4574dbe35eab8323fcffa7bd93d08a092310949e7bcec0eb196", + "name": "test/awx", + "ftype": "dir", + "chksum_type": null, + "chksum_sha256": null, "format": 1 }, { - "name": "plugins/modules/job_cancel.py", + "name": "test/awx/test_ad_hoc_wait.py", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "d64f698909919b05c9c47a65f24c861c3cabe33c039944f6120d49a2ac7d40da", + "chksum_sha256": "daed2a74d3f64fd0300255050dc8c732158db401323f44da66ccb4bf84b59633", "format": 1 }, { - "name": "plugins/modules/job_list.py", + "name": "test/awx/test_notification_template.py", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "2ea8024bfc9612c005745a13a508c40d320b4c204bf18fcd495f72789d9adb40", + "chksum_sha256": "3e9aee01e9d7d3c3662b6204422d907b78323440ebdc836b69f933336ec36b0f", "format": 1 }, { - "name": "plugins/modules/__init__.py", + "name": "test/awx/test_label.py", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + "chksum_sha256": "cd957d0b0cab6dd51539baf3fb27b659b91a8e57b20aae4c5cce7eaec9cec494", "format": 1 }, { - "name": "plugins/modules/bulk_job_launch.py", + "name": "test/awx/test_export.py", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "30167cd8d30e054fbc327308b5a9d407729c568a70bb834b8ba565969f6bc4f4", + "chksum_sha256": "c24a35265af8ff90f6456d39d0cc84cc9ce765d9fc3d45a4a8ac945e2538ff6b", "format": 1 }, { - "name": "plugins/modules/ad_hoc_command_cancel.py", + "name": "test/awx/test_module_utils.py", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "3338e10af9ccd0e4178b8e1ec1e7064b00ab90e64665f846a2123f10d9d151f4", + "chksum_sha256": "a5118e383f1370175dc7900ec3abae2ee53bd77ecf8853ca333ffcbf625b216f", "format": 1 }, { - "name": "plugins/modules/token.py", + "name": "test/awx/test_completeness.py", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "df79756cfc32e63b15e46c6bed1502c780db0257f54fcecf617960285c0f3286", + "chksum_sha256": "56bc8d4297a9c7c4fdb712fd838803cc9233023288f5555f3e3d229117604eee", "format": 1 }, { - "name": "plugins/modules/instance.py", + "name": "test/awx/test_user.py", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "ae8cbc633720f99c822f3cd5fe459605b241f0db6152fb4293f238864c0b7513", + "chksum_sha256": "f9520b058e16e4e4800d3a5f70cd28650a365fa357afa1d41a8c63bf3354027e", "format": 1 }, { - "name": "plugins/modules/bulk_host_create.py", + "name": "test/awx/test_inventory_source.py", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "ddd2c181c2e32a25a801afbe206514edd938cb372aa17a5f265f3f7eaba78996", + "chksum_sha256": "a14f69db1bf6cec594e64c2963b415560b78eac1f9cbe7d4c09586b494e11bde", "format": 1 }, { - "name": "plugins/modules/job_launch.py", + "name": "test/awx/test_group.py", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "adb57a4499f754bce74741f5a15ab5d00bf318199796180a627549c6699693e2", + "chksum_sha256": "1ecf188e82d4c848de64c8f7fd7af2d4adb6887c6a448771ff51bb43c4fa8128", "format": 1 }, { - "name": "plugins/modules/execution_environment.py", + "name": "test/awx/test_project.py", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "219ffa0fca58cbfa76c1dba8dba9d3060b1e6816c84a201241645b90b59a75c0", + "chksum_sha256": "fbb93e524df51b788f12746ffb52bf5105f67b3ae6b89403bed51ed1f2da9c12", "format": 1 }, { - "name": "plugins/modules/workflow_job_template_node.py", + "name": "test/awx/test_job.py", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "37f6e42c3ba5ab5c6df1c9c7d336e0377346cad811aee6a9ed6004f29770adb8", + "chksum_sha256": "76ba45e14438425f7511d196613928d64253e1912a45b71ea842b1cb2c3ca335", "format": 1 }, { - "name": "plugins/modules/group.py", + "name": "test/awx/test_job_template.py", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "5aaa56a12e55ed92aba4a591ef493d013df47dcd29371664837f5405ff52631f", + "chksum_sha256": "2f0e924d79cd0b2bccbe4664e27ec07cb96a274d289229d80be09687f099171e", "format": 1 }, { - "name": "plugins/modules/label.py", + "name": "test/awx/test_bulk.py", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "65e6ecc50888ebfae7498ba414325a715363676520af41f65aa8a0cecc19ea9d", + "chksum_sha256": "a10cc6ca47f1cd560e5adff57f88b4628b6ff3ec20874a751ae57d7474cfb7d5", "format": 1 }, { - "name": "plugins/modules/inventory_source_update.py", + "name": "test/awx/test_role.py", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "8a942beea88f174b16ee262e2332e26a1de2744d88fccfe79aa7b11a11fbf9dc", + "chksum_sha256": "8f0340f77fd1465cf6e267b301e44ae86c5238b05aa89bd7fff145726a83ebb4", "format": 1 }, { - "name": "plugins/modules/inventory.py", + "name": "test/awx/test_token.py", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "54620e4006a83c9641baefdbe4a8b953ee124dc1654a3ccae487461db2b853b4", + "chksum_sha256": "118145cdd5f6a03df7a7a608d5f9e510236b2a54f9bcd456f4294ba69f0f4fad", "format": 1 }, { - "name": "plugins/modules/notification_template.py", + "name": "test/awx/test_workflow_job_template_node.py", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "473f8d494ba4356c93b76ccc3b25e95ea5afd6f413ee30d244070e2e7ffd66bd", + "chksum_sha256": "0806356bfd91b28153baa63ca8cbf8f7da1125dd5150e38e73aa37c65e236f6b", "format": 1 }, { - "name": "plugins/modules/license.py", + "name": "test/awx/test_instance.py", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "d285b03abaf448db184ec0304d95206115e7d3f0cf28adba009c0c84084f5f52", + "chksum_sha256": "2b60fecf79c63341b0eece0d9941a655dfeac89b3565e78f5bb39c5ec77b91af", "format": 1 }, { - "name": "plugins/modules/controller_meta.py", + "name": "test/awx/test_credential.py", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "c66ebbe3a0eab6a9d28d517824ebf8478afdf14981c6c931f08592503c243cdd", + "chksum_sha256": "c1bfcf99f4de13b3cc4a76c7f33fa3a51fde175afff5730b4743695cbeb69643", "format": 1 }, { - "name": "plugins/modules/workflow_approval.py", + "name": "test/awx/test_credential_type.py", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "533e52fc20ca99e935154f5ba3ec30c2055f42d1c51c3bc2cf1570af7f951c33", + "chksum_sha256": "1fe388a0c19f08006c7718766d5faa79540dd3b14547ced43b5a237a2c2fd877", "format": 1 }, { - "name": "plugins/modules/import.py", + "name": "test/awx/test_credential_input_source.py", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "a7a03186251ef644ba03c49e7e23a799f8046abddb9ea20fff68dd09fe759680", + "chksum_sha256": "9637a418c0b0e59261ec0d1c206ff2d3574a41a8a169068bbf74588e3a4214b2", "format": 1 }, { - "name": "plugins/modules/team.py", + "name": "test/awx/test_instance_group.py", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "d2ace1a41e1456f7d187ad7a1d3becdddd27cd945dcee863a048add0dbfac9f6", + "chksum_sha256": "9ce22bf5e6baa63ab096c9377478f8a3af33624def33e52753342e435924e573", "format": 1 }, { - "name": "plugins/modules/export.py", + "name": "test/awx/test_organization.py", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "478b9c1a9808f40a284d733e9dd9739767bdfa5ddf6c14720bc8f325a5433195", + "chksum_sha256": "091d48906e4bf5ffaceead8f49281e05184cefce1546f387ce29a232d6694ec9", "format": 1 }, { - "name": "plugins/modules/instance_group.py", + "name": "test/awx/test_workflow_job_template.py", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "62bfe82b93ddeafcc72bf9c8a7a12fec6df00bf42d7b5e2c55de17053de276da", + "chksum_sha256": "16c8ebc74606940f6ee1f51a191f22b497c176a46e770e886bbf94bdf0c25842", "format": 1 }, { - "name": "plugins/modules/schedule.py", + "name": "test/awx/conftest.py", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "42f1c845cb65fc43b5ccc1a08d98ea4cc4b4d0aefbba3c88a454e3497a711e19", + "chksum_sha256": "8eb2d805f88e99f1c63aeb256ef1ac720831191d17a81b5b3bb2f9c06b29544e", "format": 1 }, { - "name": "plugins/modules/settings.py", + "name": "test/awx/test_settings.py", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "6a382df72aa10c2d5402a2a66b430c789b0b3588c3f1dca226f9ad09b01c9bdb", + "chksum_sha256": "436c13933936e7b80dd26c61ea1dbf492c13974f2922f1543c4fe6e6b0fab0dd", "format": 1 }, { - "name": "plugins/lookup", - "ftype": "dir", - "chksum_type": null, - "chksum_sha256": null, + "name": "test/awx/test_team.py", + "ftype": "file", + "chksum_type": "sha256", + "chksum_sha256": "cbbdbdb3be0b0d80dcfcf337ed0095774cf73ef0e937d3e8dc5abab21739db5d", "format": 1 }, { - "name": "plugins/lookup/schedule_rruleset.py", + "name": "test/awx/test_application.py", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "be96c1d747f985ea531835277bd751af13e109f326413a45fd5c5cc1093c7414", + "chksum_sha256": "a106d5fbffbe1eaec36d8247979ca637ee733a29abf94d955c48be8d2fd16842", "format": 1 }, { - "name": "plugins/lookup/schedule_rrule.py", + "name": "test/awx/test_schedule.py", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "c3ec7b8f134eca3a9f04156213b584792fc4e3397e3b9f82b5044e9ec662c7a2", + "chksum_sha256": "dadfd1c19c4c828dd84128ca484b837c6a904a09e92bcee12cb7cda408562c81", "format": 1 }, { - "name": "plugins/lookup/controller_api.py", + "name": "test/awx/test_inventory.py", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "5e79f19c9dee4fa0c3a88126a630fa6163249c332d73a44370f64836e22d4b27", + "chksum_sha256": "70eac0cf78806e37406137fcfb97e5a249fd6b091b1f18e812278573049a4111", "format": 1 }, { - "name": "requirements.txt", + "name": "TESTING.md", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "2eb11923e1347afc5075a7871e206a8f15a68471c90012f7386e9db0875e70bf", + "chksum_sha256": "4691e79c8038d8e985610fb613cd2f4799d4740b0a6ca1b72d3266528088a272", "format": 1 } ], diff --git a/ansible_collections/awx/awx/MANIFEST.json b/ansible_collections/awx/awx/MANIFEST.json index 31be1f1d8..9480679a6 100644 --- a/ansible_collections/awx/awx/MANIFEST.json +++ b/ansible_collections/awx/awx/MANIFEST.json @@ -2,7 +2,7 @@ "collection_info": { "namespace": "awx", "name": "awx", - "version": "21.14.0", + "version": "23.9.0", "authors": [ "AWX Project Contributors <awx-project@googlegroups.com>" ], @@ -16,7 +16,7 @@ ], "description": "Ansible content that interacts with the AWX or Automation Platform Controller API.", "license": [ - "GPL-3.0-only" + "GPL-3.0-or-later" ], "license_file": null, "dependencies": {}, @@ -29,7 +29,7 @@ "name": "FILES.json", "ftype": "file", "chksum_type": "sha256", - "chksum_sha256": "f4ba3e6ff1189b8f123c73022f3b612c1bcc2f3cc136f5509c722df28a709869", + "chksum_sha256": "b47d01dc90de9b4af46e366ac75ac9d533d4abfcc93a81b6956b098d2989f587", "format": 1 }, "format": 1 diff --git a/ansible_collections/awx/awx/README.md b/ansible_collections/awx/awx/README.md index ea9e85e11..c3b6f9d52 100644 --- a/ansible_collections/awx/awx/README.md +++ b/ansible_collections/awx/awx/README.md @@ -69,6 +69,7 @@ Notable releases of the `awx.awx` collection: - 11.0.0 has no non-deprecated modules that depend on the deprecated `tower-cli` [PyPI](https://pypi.org/project/ansible-tower-cli/). - 19.2.1 large renaming purged "tower" names (like options and module names), adding redirects for old names - 21.11.0 "tower" modules deprecated and symlinks removed. + - X.X.X added support of named URLs to all modules. Anywhere that previously accepted name or id can also support named URLs - 0.0.1-devel is the version you should see if installing from source, which is intended for development and expected to be unstable. The following notes are changes that may require changes to playbooks: @@ -112,7 +113,7 @@ Ansible source, set up a dedicated virtual environment: ``` mkvirtualenv my_new_venv -# may need to replace psycopg2 with psycopg2-binary in requirements/requirements.txt +# may need to replace psycopg3 with psycopg3-binary in requirements/requirements.txt pip install -r requirements/requirements.txt -r requirements/requirements_dev.txt -r requirements/requirements_git.txt make clean-api pip install -e <path to your Ansible> diff --git a/ansible_collections/awx/awx/meta/runtime.yml b/ansible_collections/awx/awx/meta/runtime.yml index 3e50a52e6..18fa4b592 100644 --- a/ansible_collections/awx/awx/meta/runtime.yml +++ b/ansible_collections/awx/awx/meta/runtime.yml @@ -8,6 +8,7 @@ action_groups: - application - bulk_job_launch - bulk_host_create + - bulk_host_delete - controller_meta - credential_input_source - credential diff --git a/ansible_collections/awx/awx/plugins/doc_fragments/auth.py b/ansible_collections/awx/awx/plugins/doc_fragments/auth.py index 3cab718a7..763fe94dc 100644 --- a/ansible_collections/awx/awx/plugins/doc_fragments/auth.py +++ b/ansible_collections/awx/awx/plugins/doc_fragments/auth.py @@ -50,6 +50,11 @@ options: - If value not set, will try environment variable C(CONTROLLER_VERIFY_SSL) and then config files type: bool aliases: [ tower_verify_ssl ] + request_timeout: + description: + - Specify the timeout Ansible should use in requests to the controller host. + - Defaults to 10s, but this is handled by the shared module_utils code + type: float controller_config_file: description: - Path to the controller config file. diff --git a/ansible_collections/awx/awx/plugins/doc_fragments/auth_plugin.py b/ansible_collections/awx/awx/plugins/doc_fragments/auth_plugin.py index 5a3a12b0e..b46eaf6bb 100644 --- a/ansible_collections/awx/awx/plugins/doc_fragments/auth_plugin.py +++ b/ansible_collections/awx/awx/plugins/doc_fragments/auth_plugin.py @@ -68,6 +68,14 @@ options: why: Collection name change alternatives: 'CONTROLLER_VERIFY_SSL' aliases: [ validate_certs ] + request_timeout: + description: + - Specify the timeout Ansible should use in requests to the controller host. + - Defaults to 10 seconds + - This will not work with the export or import modules. + type: float + env: + - name: CONTROLLER_REQUEST_TIMEOUT notes: - If no I(config_file) is provided we will attempt to use the tower-cli library diff --git a/ansible_collections/awx/awx/plugins/lookup/schedule_rruleset.py b/ansible_collections/awx/awx/plugins/lookup/schedule_rruleset.py index b45d861db..07e990800 100644 --- a/ansible_collections/awx/awx/plugins/lookup/schedule_rruleset.py +++ b/ansible_collections/awx/awx/plugins/lookup/schedule_rruleset.py @@ -214,7 +214,7 @@ class LookupModule(LookupBase): if not isinstance(rule[field_name], list): rule[field_name] = rule[field_name].split(',') for value in rule[field_name]: - value = value.strip() + value = value.strip().lower() if value not in valid_list: raise AnsibleError('In rule {0} {1} must only contain values in {2}'.format(rule_number, field_name, ', '.join(valid_list.keys()))) return_values.append(valid_list[value]) diff --git a/ansible_collections/awx/awx/plugins/module_utils/controller_api.py b/ansible_collections/awx/awx/plugins/module_utils/controller_api.py index abf9273d7..8bc3955ce 100644 --- a/ansible_collections/awx/awx/plugins/module_utils/controller_api.py +++ b/ansible_collections/awx/awx/plugins/module_utils/controller_api.py @@ -10,14 +10,14 @@ from ansible.module_utils.six import raise_from, string_types from ansible.module_utils.six.moves import StringIO from ansible.module_utils.six.moves.urllib.error import HTTPError from ansible.module_utils.six.moves.http_cookiejar import CookieJar -from ansible.module_utils.six.moves.urllib.parse import urlparse, urlencode +from ansible.module_utils.six.moves.urllib.parse import urlparse, urlencode, quote from ansible.module_utils.six.moves.configparser import ConfigParser, NoOptionError from socket import getaddrinfo, IPPROTO_TCP import time import re from json import loads, dumps from os.path import isfile, expanduser, split, join, exists, isdir -from os import access, R_OK, getcwd +from os import access, R_OK, getcwd, environ try: @@ -47,47 +47,31 @@ class ItemNotDefined(Exception): class ControllerModule(AnsibleModule): url = None AUTH_ARGSPEC = dict( - controller_host=dict( - required=False, - aliases=['tower_host'], - fallback=(env_fallback, ['CONTROLLER_HOST', 'TOWER_HOST'])), - controller_username=dict( - required=False, - aliases=['tower_username'], - fallback=(env_fallback, ['CONTROLLER_USERNAME', 'TOWER_USERNAME'])), - controller_password=dict( - no_log=True, - aliases=['tower_password'], - required=False, - fallback=(env_fallback, ['CONTROLLER_PASSWORD', 'TOWER_PASSWORD'])), - validate_certs=dict( - type='bool', - aliases=['tower_verify_ssl'], - required=False, - fallback=(env_fallback, ['CONTROLLER_VERIFY_SSL', 'TOWER_VERIFY_SSL'])), + controller_host=dict(required=False, aliases=['tower_host'], fallback=(env_fallback, ['CONTROLLER_HOST', 'TOWER_HOST'])), + controller_username=dict(required=False, aliases=['tower_username'], fallback=(env_fallback, ['CONTROLLER_USERNAME', 'TOWER_USERNAME'])), + controller_password=dict(no_log=True, aliases=['tower_password'], required=False, fallback=(env_fallback, ['CONTROLLER_PASSWORD', 'TOWER_PASSWORD'])), + validate_certs=dict(type='bool', aliases=['tower_verify_ssl'], required=False, fallback=(env_fallback, ['CONTROLLER_VERIFY_SSL', 'TOWER_VERIFY_SSL'])), + request_timeout=dict(type='float', required=False, fallback=(env_fallback, ['CONTROLLER_REQUEST_TIMEOUT'])), controller_oauthtoken=dict( - type='raw', - no_log=True, - aliases=['tower_oauthtoken'], - required=False, - fallback=(env_fallback, ['CONTROLLER_OAUTH_TOKEN', 'TOWER_OAUTH_TOKEN'])), - controller_config_file=dict( - type='path', - aliases=['tower_config_file'], - required=False, - default=None), + type='raw', no_log=True, aliases=['tower_oauthtoken'], required=False, fallback=(env_fallback, ['CONTROLLER_OAUTH_TOKEN', 'TOWER_OAUTH_TOKEN']) + ), + controller_config_file=dict(type='path', aliases=['tower_config_file'], required=False, default=None), ) + # Associations of these types are ordered and have special consideration in the modified associations function + ordered_associations = ['instance_groups', 'galaxy_credentials'] short_params = { 'host': 'controller_host', 'username': 'controller_username', 'password': 'controller_password', 'verify_ssl': 'validate_certs', + 'request_timeout': 'request_timeout', 'oauth_token': 'controller_oauthtoken', } host = '127.0.0.1' username = None password = None verify_ssl = True + request_timeout = 10 oauth_token = None oauth_token_id = None authenticated = False @@ -152,9 +136,11 @@ class ControllerModule(AnsibleModule): self.url.hostname.replace(char, "") # Try to resolve the hostname try: - addrinfolist = getaddrinfo(self.url.hostname, self.url.port, proto=IPPROTO_TCP) - for family, kind, proto, canonical, sockaddr in addrinfolist: - sockaddr[0] + proxy_env_var_name = "{0}_proxy".format(self.url.scheme) + if not environ.get(proxy_env_var_name) and not environ.get(proxy_env_var_name.upper()): + addrinfolist = getaddrinfo(self.url.hostname, self.url.port, proto=IPPROTO_TCP) + for family, kind, proto, canonical, sockaddr in addrinfolist: + sockaddr[0] except Exception as e: self.fail_json(msg="Unable to resolve controller_host ({1}): {0}".format(self.url.hostname, e)) @@ -305,7 +291,7 @@ class ControllerModule(AnsibleModule): class ControllerAPIModule(ControllerModule): # TODO: Move the collection version check into controller_module.py # This gets set by the make process so whatever is in here is irrelevant - _COLLECTION_VERSION = "21.14.0" + _COLLECTION_VERSION = "23.9.0" _COLLECTION_TYPE = "awx" # This maps the collections type (awx/tower) to the values returned by the API # Those values can be found in awx/api/generics.py line 204 @@ -320,10 +306,8 @@ class ControllerAPIModule(ControllerModule): def __init__(self, argument_spec, direct_params=None, error_callback=None, warn_callback=None, **kwargs): kwargs['supports_check_mode'] = True - super().__init__( - argument_spec=argument_spec, direct_params=direct_params, error_callback=error_callback, warn_callback=warn_callback, **kwargs - ) - self.session = Request(cookies=CookieJar(), validate_certs=self.verify_ssl) + super().__init__(argument_spec=argument_spec, direct_params=direct_params, error_callback=error_callback, warn_callback=warn_callback, **kwargs) + self.session = Request(cookies=CookieJar(), timeout=self.request_timeout, validate_certs=self.verify_ssl) if 'update_secrets' in self.params: self.update_secrets = self.params.pop('update_secrets') @@ -331,11 +315,6 @@ class ControllerAPIModule(ControllerModule): self.update_secrets = True @staticmethod - def param_to_endpoint(name): - exceptions = {'inventory': 'inventories', 'target_team': 'teams', 'workflow': 'workflow_job_templates'} - return exceptions.get(name, '{0}s'.format(name)) - - @staticmethod def get_name_field_from_endpoint(endpoint): return ControllerAPIModule.IDENTITY_FIELDS.get(endpoint, 'name') @@ -405,31 +384,53 @@ class ControllerAPIModule(ControllerModule): response['json']['next'] = next_page return response - def get_one(self, endpoint, name_or_id=None, allow_none=True, **kwargs): + def get_one(self, endpoint, name_or_id=None, allow_none=True, check_exists=False, **kwargs): new_kwargs = kwargs.copy() - if name_or_id: - name_field = self.get_name_field_from_endpoint(endpoint) - new_data = kwargs.get('data', {}).copy() - if name_field in new_data: - self.fail_json(msg="You can't specify the field {0} in your search data if using the name_or_id field".format(name_field)) + response = None - try: - new_data['or__id'] = int(name_or_id) - new_data['or__{0}'.format(name_field)] = name_or_id - except ValueError: - # If we get a value error, then we didn't have an integer so we can just pass and fall down to the fail - new_data[name_field] = name_or_id - new_kwargs['data'] = new_data - - response = self.get_endpoint(endpoint, **new_kwargs) - if response['status_code'] != 200: - fail_msg = "Got a {0} response when trying to get one from {1}".format(response['status_code'], endpoint) - if 'detail' in response.get('json', {}): - fail_msg += ', detail: {0}'.format(response['json']['detail']) - self.fail_json(msg=fail_msg) - - if 'count' not in response['json'] or 'results' not in response['json']: - self.fail_json(msg="The endpoint did not provide count and results") + # A named URL is pretty unique so if we have a ++ in the name then lets start by looking for that + # This also needs to go first because if there was data passed in kwargs and we do the next lookup first there may be results + if name_or_id is not None and '++' in name_or_id: + # Maybe someone gave us a named URL so lets see if we get anything from that. + url_quoted_name = quote(name_or_id, safe="+") + named_endpoint = '{0}/{1}/'.format(endpoint, url_quoted_name) + named_response = self.get_endpoint(named_endpoint) + + if named_response['status_code'] == 200 and 'json' in named_response: + # We found a named item but we expect to deal with a list view so mock that up + response = { + 'json': { + 'count': 1, + 'results': [named_response['json']], + } + } + + # Since we didn't have a named URL, lets try and find it with a general search + if response is None: + if name_or_id: + name_field = self.get_name_field_from_endpoint(endpoint) + new_data = kwargs.get('data', {}).copy() + if name_field in new_data: + self.fail_json(msg="You can't specify the field {0} in your search data if using the name_or_id field".format(name_field)) + + try: + new_data['or__id'] = int(name_or_id) + new_data['or__{0}'.format(name_field)] = name_or_id + except ValueError: + # If we get a value error, then we didn't have an integer so we can just pass and fall down to the fail + new_data[name_field] = name_or_id + new_kwargs['data'] = new_data + + response = self.get_endpoint(endpoint, **new_kwargs) + + if response['status_code'] != 200: + fail_msg = "Got a {0} response when trying to get one from {1}".format(response['status_code'], endpoint) + if 'detail' in response.get('json', {}): + fail_msg += ', detail: {0}'.format(response['json']['detail']) + self.fail_json(msg=fail_msg) + + if 'count' not in response['json'] or 'results' not in response['json']: + self.fail_json(msg="The endpoint did not provide count and results") if response['json']['count'] == 0: if allow_none: @@ -446,6 +447,10 @@ class ControllerAPIModule(ControllerModule): # Or we weren't running with a or search and just got back too many to begin with. self.fail_wanted_one(response, endpoint, new_kwargs.get('data')) + if check_exists: + self.json_output['id'] = response['json']['results'][0]['id'] + self.exit_json(**self.json_output) + return response['json']['results'][0] def fail_wanted_one(self, response, endpoint, query_params): @@ -453,7 +458,8 @@ class ControllerAPIModule(ControllerModule): if len(sample['json']['results']) > 1: sample['json']['results'] = sample['json']['results'][:2] + ['...more results snipped...'] url = self.build_url(endpoint, query_params) - display_endpoint = url.geturl()[len(self.host):] # truncate to not include the base URL + host_length = len(self.host) + display_endpoint = url.geturl()[host_length:] # truncate to not include the base URL self.fail_json( msg="Request to {0} returned {1} items, expected 1".format(display_endpoint, response['json']['count']), query=query_params, @@ -497,7 +503,14 @@ class ControllerAPIModule(ControllerModule): data = dumps(kwargs.get('data', {})) try: - response = self.session.open(method, url.geturl(), headers=headers, validate_certs=self.verify_ssl, follow_redirects=True, data=data) + response = self.session.open( + method, url.geturl(), + headers=headers, + timeout=self.request_timeout, + validate_certs=self.verify_ssl, + follow_redirects=True, + data=data + ) except (SSLValidationError) as ssl_err: self.fail_json(msg="Could not establish a secure connection to your host ({1}): {0}.".format(url.netloc, ssl_err)) except (ConnectionError) as con_err: @@ -551,13 +564,7 @@ class ControllerAPIModule(ControllerModule): controller_version = response.info().getheader('X-API-Product-Version', None) parsed_collection_version = Version(self._COLLECTION_VERSION).version - if not controller_version: - self.warn( - "You are using the {0} version of this collection but connecting to a controller that did not return a version".format( - self._COLLECTION_VERSION - ) - ) - else: + if controller_version: parsed_controller_version = Version(controller_version).version if controller_type == 'AWX': collection_compare_ver = parsed_collection_version[0] @@ -615,6 +622,7 @@ class ControllerAPIModule(ControllerModule): 'POST', api_token_url, validate_certs=self.verify_ssl, + timeout=self.request_timeout, follow_redirects=True, force_basic_auth=True, url_username=self.username, @@ -700,17 +708,26 @@ class ControllerAPIModule(ControllerModule): response = self.get_all_endpoint(association_endpoint) existing_associated_ids = [association['id'] for association in response['json']['results']] - # Disassociate anything that is in existing_associated_ids but not in new_association_list - ids_to_remove = list(set(existing_associated_ids) - set(new_association_list)) - for an_id in ids_to_remove: + # Some associations can be ordered (like galaxy credentials) + if association_endpoint.strip('/').split('/')[-1] in self.ordered_associations: + if existing_associated_ids == new_association_list: + return # If the current associations EXACTLY match the desired associations then we can return + removal_list = existing_associated_ids # because of ordering, we have to remove everything + addition_list = new_association_list # re-add everything back in-order + else: + if set(existing_associated_ids) == set(new_association_list): + return + removal_list = set(existing_associated_ids) - set(new_association_list) + addition_list = set(new_association_list) - set(existing_associated_ids) + + for an_id in removal_list: response = self.post_endpoint(association_endpoint, **{'data': {'id': int(an_id), 'disassociate': True}}) if response['status_code'] == 204: self.json_output['changed'] = True else: self.fail_json(msg="Failed to disassociate item {0}".format(response['json'].get('detail', response['json']))) - # Associate anything that is in new_association_list but not in `association` - for an_id in list(set(new_association_list) - set(existing_associated_ids)): + for an_id in addition_list: response = self.post_endpoint(association_endpoint, **{'data': {'id': int(an_id)}}) if response['status_code'] == 204: self.json_output['changed'] = True @@ -963,6 +980,15 @@ class ControllerAPIModule(ControllerModule): def create_or_update_if_needed( self, existing_item, new_item, endpoint=None, item_type='unknown', on_create=None, on_update=None, auto_exit=True, associations=None ): + # Remove boolean values of certain specific types + # this is needed so that boolean fields will not get a false value when not provided + for key in list(new_item.keys()): + if key in self.argument_spec: + param_spec = self.argument_spec[key] + if 'type' in param_spec and param_spec['type'] == 'bool': + if new_item[key] is None: + new_item.pop(key) + if existing_item: return self.update_if_needed(existing_item, new_item, on_update=on_update, auto_exit=auto_exit, associations=associations) else: @@ -975,17 +1001,14 @@ class ControllerAPIModule(ControllerModule): # Attempt to delete our current token from /api/v2/tokens/ # Post to the tokens endpoint with baisc auth to try and get a token endpoint = self.url_prefix.rstrip('/') + '/api/v2/tokens/{0}/'.format(self.oauth_token_id) - api_token_url = ( - self.url._replace( - path=endpoint, query=None # in error cases, fail_json exists before exception handling - ) - ).geturl() + api_token_url = (self.url._replace(path=endpoint, query=None)).geturl() # in error cases, fail_json exists before exception handling try: self.session.open( 'DELETE', api_token_url, validate_certs=self.verify_ssl, + timeout=self.request_timeout, follow_redirects=True, force_basic_auth=True, url_username=self.username, diff --git a/ansible_collections/awx/awx/plugins/module_utils/tower_legacy.py b/ansible_collections/awx/awx/plugins/module_utils/tower_legacy.py deleted file mode 100644 index 84205c0f8..000000000 --- a/ansible_collections/awx/awx/plugins/module_utils/tower_legacy.py +++ /dev/null @@ -1,119 +0,0 @@ -# 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. -# -# Copyright (c), Wayne Witzel III <wayne@riotousliving.com> -# All rights reserved. -# -# 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. - -from __future__ import absolute_import, division, print_function - -__metaclass__ = type - -import os -import traceback - -TOWER_CLI_IMP_ERR = None -try: - import tower_cli.utils.exceptions as exc - from tower_cli.utils import parser - from tower_cli.api import client - - HAS_TOWER_CLI = True -except ImportError: - TOWER_CLI_IMP_ERR = traceback.format_exc() - HAS_TOWER_CLI = False - -from ansible.module_utils.basic import AnsibleModule, missing_required_lib - - -def tower_auth_config(module): - """ - `tower_auth_config` attempts to load the tower-cli.cfg file - specified from the `tower_config_file` parameter. If found, - if returns the contents of the file as a dictionary, else - it will attempt to fetch values from the module params and - only pass those values that have been set. - """ - config_file = module.params.pop('tower_config_file', None) - if config_file: - if not os.path.exists(config_file): - module.fail_json(msg='file not found: %s' % config_file) - if os.path.isdir(config_file): - module.fail_json(msg='directory can not be used as config file: %s' % config_file) - - with open(config_file, 'r') as f: - return parser.string_to_dict(f.read()) - else: - auth_config = {} - host = module.params.pop('tower_host', None) - if host: - auth_config['host'] = host - username = module.params.pop('tower_username', None) - if username: - auth_config['username'] = username - password = module.params.pop('tower_password', None) - if password: - auth_config['password'] = password - module.params.pop('tower_verify_ssl', None) # pop alias if used - verify_ssl = module.params.pop('validate_certs', None) - if verify_ssl is not None: - auth_config['verify_ssl'] = verify_ssl - return auth_config - - -def tower_check_mode(module): - '''Execute check mode logic for Ansible Tower modules''' - if module.check_mode: - try: - result = client.get('/ping').json() - module.exit_json(changed=True, tower_version='{0}'.format(result['version'])) - except (exc.ServerError, exc.ConnectionError, exc.BadRequest) as excinfo: - module.fail_json(changed=False, msg='Failed check mode: {0}'.format(excinfo)) - - -class TowerLegacyModule(AnsibleModule): - def __init__(self, argument_spec, **kwargs): - args = dict( - tower_host=dict(), - tower_username=dict(), - tower_password=dict(no_log=True), - validate_certs=dict(type='bool', aliases=['tower_verify_ssl']), - tower_config_file=dict(type='path'), - ) - args.update(argument_spec) - - kwargs.setdefault('mutually_exclusive', []) - kwargs['mutually_exclusive'].extend( - ( - ('tower_config_file', 'tower_host'), - ('tower_config_file', 'tower_username'), - ('tower_config_file', 'tower_password'), - ('tower_config_file', 'validate_certs'), - ) - ) - - super().__init__(argument_spec=args, **kwargs) - - if not HAS_TOWER_CLI: - self.fail_json(msg=missing_required_lib('ansible-tower-cli'), exception=TOWER_CLI_IMP_ERR) diff --git a/ansible_collections/awx/awx/plugins/modules/ad_hoc_command.py b/ansible_collections/awx/awx/plugins/modules/ad_hoc_command.py index bfe22eb89..5864d392a 100644 --- a/ansible_collections/awx/awx/plugins/modules/ad_hoc_command.py +++ b/ansible_collections/awx/awx/plugins/modules/ad_hoc_command.py @@ -29,12 +29,12 @@ options: choices: [ 'run', 'check' ] execution_environment: description: - - Execution Environment to use for the ad hoc command. + - Execution Environment name, ID, or named URL to use for the ad hoc command. required: False type: str inventory: description: - - Inventory to use for the ad hoc command. + - Inventory name, ID, or named URL to use for the ad hoc command. required: True type: str limit: @@ -43,7 +43,7 @@ options: type: str credential: description: - - Credential to use for ad hoc command. + - Credential name, ID, or named URL to use for ad hoc command. required: True type: str module_name: @@ -95,6 +95,13 @@ extends_documentation_fragment: awx.awx.auth ''' EXAMPLES = ''' +- name: Launch an Ad Hoc Command waiting for it to finish + ad_hoc_command: + inventory: Demo Inventory + credential: Demo Credential + module_name: command + module_args: echo I <3 Ansible + wait: true ''' RETURN = ''' @@ -111,6 +118,7 @@ status: ''' from ..module_utils.controller_api import ControllerAPIModule +import json def main(): @@ -145,6 +153,7 @@ def main(): wait = module.params.get('wait') interval = module.params.get('interval') timeout = module.params.get('timeout') + execution_environment = module.params.get('execution_environment') # Create a datastructure to pass into our command launch post_data = { @@ -153,11 +162,17 @@ def main(): } for arg in ['job_type', 'limit', 'forks', 'verbosity', 'extra_vars', 'become_enabled', 'diff_mode']: if module.params.get(arg): - post_data[arg] = module.params.get(arg) + # extra_var can receive a dict or a string, if a dict covert it to a string + if arg == 'extra_vars' and type(module.params.get(arg)) is not str: + post_data[arg] = json.dumps(module.params.get(arg)) + else: + post_data[arg] = module.params.get(arg) # Attempt to look up the related items the user specified (these will fail the module if not found) post_data['inventory'] = module.resolve_name_to_id('inventories', inventory) post_data['credential'] = module.resolve_name_to_id('credentials', credential) + if execution_environment: + post_data['execution_environment'] = module.resolve_name_to_id('execution_environments', execution_environment) # Launch the ad hoc command results = module.post_endpoint('ad_hoc_commands', **{'data': post_data}) diff --git a/ansible_collections/awx/awx/plugins/modules/application.py b/ansible_collections/awx/awx/plugins/modules/application.py index e35e99dfe..7a31522a0 100644 --- a/ansible_collections/awx/awx/plugins/modules/application.py +++ b/ansible_collections/awx/awx/plugins/modules/application.py @@ -48,7 +48,7 @@ options: required: False organization: description: - - Name of organization for application. + - Name, ID, or named URL of organization for application. type: str required: True redirect_uris: @@ -60,7 +60,7 @@ options: description: - Desired state of the resource. default: "present" - choices: ["present", "absent"] + choices: ["present", "absent", "exists"] type: str skip_authorization: description: @@ -106,7 +106,7 @@ def main(): client_type=dict(choices=['public', 'confidential']), organization=dict(required=True), redirect_uris=dict(type="list", elements='str'), - state=dict(choices=['present', 'absent'], default='present'), + state=dict(choices=['present', 'absent', 'exists'], default='present'), skip_authorization=dict(type='bool'), ) @@ -127,7 +127,7 @@ def main(): org_id = module.resolve_name_to_id('organizations', organization) # Attempt to look up application based on the provided name and org ID - application = module.get_one('applications', name_or_id=name, **{'data': {'organization': org_id}}) + application = module.get_one('applications', name_or_id=name, check_exists=(state == 'exists'), **{'data': {'organization': org_id}}) if state == 'absent': # If the state was absent we can let the module delete it if needed, the module will handle exiting from this diff --git a/ansible_collections/awx/awx/plugins/modules/bulk_host_create.py b/ansible_collections/awx/awx/plugins/modules/bulk_host_create.py index c2cb6992d..dcd32bf44 100644 --- a/ansible_collections/awx/awx/plugins/modules/bulk_host_create.py +++ b/ansible_collections/awx/awx/plugins/modules/bulk_host_create.py @@ -48,7 +48,7 @@ options: type: str inventory: description: - - Inventory name or ID the hosts should be made a member of. + - Inventory name, ID, or named URL the hosts should be made a member of. required: True type: str extends_documentation_fragment: awx.awx.auth diff --git a/ansible_collections/awx/awx/plugins/modules/bulk_host_delete.py b/ansible_collections/awx/awx/plugins/modules/bulk_host_delete.py new file mode 100644 index 000000000..12468e602 --- /dev/null +++ b/ansible_collections/awx/awx/plugins/modules/bulk_host_delete.py @@ -0,0 +1,65 @@ +#!/usr/bin/python +# coding: utf-8 -*- + +# 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 + +ANSIBLE_METADATA = {'metadata_version': '1.1', 'status': ['preview'], 'supported_by': 'community'} + +DOCUMENTATION = ''' +--- +module: bulk_host_delete +author: "Avi Layani (@Avilir)" +short_description: Bulk host delete in Automation Platform Controller +description: + - Single-request bulk host deletion in Automation Platform Controller. + - Provides a way to delete many hosts at once from inventories in Controller. +options: + hosts: + description: + - List of hosts id's to delete from inventory. + required: True + type: list + elements: int +extends_documentation_fragment: awx.awx.auth +''' + +EXAMPLES = ''' +- name: Bulk host delete + bulk_host_delete: + hosts: + - 1 + - 2 +''' + +from ..module_utils.controller_api import ControllerAPIModule + + +def main(): + # Any additional arguments that are not fields of the item can be added here + argument_spec = dict( + hosts=dict(required=True, type='list', elements='int'), + ) + + # Create a module for ourselves + module = ControllerAPIModule(argument_spec=argument_spec) + + # Extract our parameters + hosts = module.params.get('hosts') + + # Delete the hosts + result = module.post_endpoint("bulk/host_delete", data={"hosts": hosts}) + + if result['status_code'] != 201: + module.fail_json(msg="Failed to delete hosts, see response for details", response=result) + + module.json_output['changed'] = True + + module.exit_json(**module.json_output) + + +if __name__ == '__main__': + main() diff --git a/ansible_collections/awx/awx/plugins/modules/bulk_job_launch.py b/ansible_collections/awx/awx/plugins/modules/bulk_job_launch.py index 8aa5fca25..00fa338dd 100644 --- a/ansible_collections/awx/awx/plugins/modules/bulk_job_launch.py +++ b/ansible_collections/awx/awx/plugins/modules/bulk_job_launch.py @@ -128,7 +128,7 @@ options: type: str inventory: description: - - Inventory name or ID to use for the jobs ran within the bulk job, only used if prompt for inventory is set. + - Inventory name, ID, or named URL to use for the jobs ran within the bulk job, only used if prompt for inventory is set. type: str scm_branch: description: @@ -186,6 +186,9 @@ EXAMPLES = ''' food: carrot color: orange limit: bar + credentials: + - "My Credential" + - "suplementary cred" extra_vars: # these override / extend extra_data at the job level food: grape animal: owl diff --git a/ansible_collections/awx/awx/plugins/modules/credential.py b/ansible_collections/awx/awx/plugins/modules/credential.py index 4e7f02e55..b5fbfe9a4 100644 --- a/ansible_collections/awx/awx/plugins/modules/credential.py +++ b/ansible_collections/awx/awx/plugins/modules/credential.py @@ -45,7 +45,8 @@ options: type: str organization: description: - - Organization that should own the credential. + - Organization name, ID, or named URL that should own the credential. + - This parameter is mutually exclusive with C(team) and C(user). type: str credential_type: description: @@ -57,6 +58,7 @@ options: Insights, Machine, Microsoft Azure Key Vault, Microsoft Azure Resource Manager, Network, OpenShift or Kubernetes API Bearer Token, OpenStack, Red Hat Ansible Automation Platform, Red Hat Satellite 6, Red Hat Virtualization, Source Control, Thycotic DevOps Secrets Vault, Thycotic Secret Server, Vault, VMware vCenter, or a custom credential type + required: True type: str inputs: description: @@ -87,21 +89,23 @@ options: update_secrets: description: - C(true) will always update encrypted values. - - C(false) will only updated encrypted values if a change is absolutely known to be needed. + - C(false) will only update encrypted values if a change is absolutely known to be needed. type: bool default: true user: description: - - User that should own this credential. + - User name, ID, or named URL that should own this credential. + - This parameter is mutually exclusive with C(organization) and C(team). type: str team: description: - - Team that should own this credential. + - Team name, ID, or named URL that should own this credential. + - This parameter is mutually exclusive with C(organization) and C(user). type: str state: description: - - Desired state of the resource. - choices: ["present", "absent"] + - Desired state of the resource. C(exists) will not modify the resource if it is present. + choices: ["present", "absent", "exists"] default: "present" type: str @@ -211,16 +215,21 @@ def main(): copy_from=dict(), description=dict(), organization=dict(), - credential_type=dict(), + credential_type=dict(required=True), inputs=dict(type='dict', no_log=True), update_secrets=dict(type='bool', default=True, no_log=False), user=dict(), team=dict(), - state=dict(choices=['present', 'absent'], default='present'), + state=dict(choices=['present', 'absent', 'exists'], default='present'), ) + mutually_exclusive = [("organization", "user", "team")] + # Create a module for ourselves - module = ControllerAPIModule(argument_spec=argument_spec) + module = ControllerAPIModule( + argument_spec=argument_spec, + mutually_exclusive=mutually_exclusive + ) # Extract our parameters name = module.params.get('name') @@ -247,7 +256,7 @@ def main(): if organization: lookup_data['organization'] = org_id - credential = module.get_one('credentials', name_or_id=name, **{'data': lookup_data}) + credential = module.get_one('credentials', name_or_id=name, check_exists=(state == 'exists'), **{'data': lookup_data}) # Attempt to look up credential to copy based on the provided name if copy_from: diff --git a/ansible_collections/awx/awx/plugins/modules/credential_input_source.py b/ansible_collections/awx/awx/plugins/modules/credential_input_source.py index c7ace4f72..e40e398a1 100644 --- a/ansible_collections/awx/awx/plugins/modules/credential_input_source.py +++ b/ansible_collections/awx/awx/plugins/modules/credential_input_source.py @@ -38,17 +38,17 @@ options: type: dict target_credential: description: - - The credential which will have its input defined by this source + - The credential name, ID, or named URL which will have its input defined by this source required: true type: str source_credential: description: - - The credential which is the source of the credential lookup + - The credential name, ID, or named URL which is the source of the credential lookup type: str state: description: - Desired state of the resource. - choices: ["present", "absent"] + choices: ["present", "absent", "exists"] default: "present" type: str @@ -80,7 +80,7 @@ def main(): target_credential=dict(required=True), source_credential=dict(), metadata=dict(type="dict"), - state=dict(choices=['present', 'absent'], default='present'), + state=dict(choices=['present', 'absent', 'exists'], default='present'), ) # Create a module for ourselves @@ -101,7 +101,7 @@ def main(): 'target_credential': target_credential_id, 'input_field_name': input_field_name, } - credential_input_source = module.get_one('credential_input_sources', **{'data': lookup_data}) + credential_input_source = module.get_one('credential_input_sources', check_exists=(state == 'exists'), **{'data': lookup_data}) if state == 'absent': module.delete_if_needed(credential_input_source) diff --git a/ansible_collections/awx/awx/plugins/modules/credential_type.py b/ansible_collections/awx/awx/plugins/modules/credential_type.py index f6b56d0ea..9407eaac3 100644 --- a/ansible_collections/awx/awx/plugins/modules/credential_type.py +++ b/ansible_collections/awx/awx/plugins/modules/credential_type.py @@ -59,7 +59,7 @@ options: description: - Desired state of the resource. default: "present" - choices: ["present", "absent"] + choices: ["present", "absent", "exists"] type: str extends_documentation_fragment: awx.awx.auth ''' @@ -98,7 +98,7 @@ def main(): kind=dict(choices=list(KIND_CHOICES.keys())), inputs=dict(type='dict'), injectors=dict(type='dict'), - state=dict(choices=['present', 'absent'], default='present'), + state=dict(choices=['present', 'absent', 'exists'], default='present'), ) # Create a module for ourselves @@ -124,7 +124,7 @@ def main(): credential_type_params['injectors'] = module.params.get('injectors') # Attempt to look up credential_type based on the provided name - credential_type = module.get_one('credential_types', name_or_id=name) + credential_type = module.get_one('credential_types', name_or_id=name, check_exists=(state == 'exists')) if state == 'absent': # If the state was absent we can let the module delete it if needed, the module will handle exiting from this diff --git a/ansible_collections/awx/awx/plugins/modules/execution_environment.py b/ansible_collections/awx/awx/plugins/modules/execution_environment.py index 552c80572..38c5ecb51 100644 --- a/ansible_collections/awx/awx/plugins/modules/execution_environment.py +++ b/ansible_collections/awx/awx/plugins/modules/execution_environment.py @@ -33,7 +33,6 @@ options: image: description: - The fully qualified url of the container image. - required: True type: str description: description: @@ -41,16 +40,16 @@ options: type: str organization: description: - - The organization the execution environment belongs to. + - The organization name, ID, or named URL that the execution environment belongs to. type: str credential: description: - - Name of the credential to use for the execution environment. + - Name, ID, or named URL of the credential to use for the execution environment. type: str state: description: - Desired state of the resource. - choices: ["present", "absent"] + choices: ["present", "absent", "exists"] default: "present" type: str pull: @@ -79,11 +78,11 @@ def main(): argument_spec = dict( name=dict(required=True), new_name=dict(), - image=dict(required=True), + image=dict(), description=dict(), organization=dict(), credential=dict(), - state=dict(choices=['present', 'absent'], default='present'), + state=dict(choices=['present', 'absent', 'exists'], default='present'), # NOTE: Default for pull differs from API (which is blank by default) pull=dict(choices=['always', 'missing', 'never'], default='missing'), ) @@ -99,7 +98,7 @@ def main(): state = module.params.get('state') pull = module.params.get('pull') - existing_item = module.get_one('execution_environments', name_or_id=name) + existing_item = module.get_one('execution_environments', name_or_id=name, check_exists=(state == 'exists')) if state == 'absent': module.delete_if_needed(existing_item) diff --git a/ansible_collections/awx/awx/plugins/modules/export.py b/ansible_collections/awx/awx/plugins/modules/export.py index d04e99605..1080b4889 100644 --- a/ansible_collections/awx/awx/plugins/modules/export.py +++ b/ansible_collections/awx/awx/plugins/modules/export.py @@ -28,72 +28,72 @@ options: default: 'False' organizations: description: - - organization names to export + - organization names, IDs, or named URLs to export type: list elements: str users: description: - - user names to export + - user names, IDs, or named URLs to export type: list elements: str teams: description: - - team names to export + - team names, IDs, or named URLs to export type: list elements: str credential_types: description: - - credential type names to export + - credential type names, IDs, or named URLs to export type: list elements: str credentials: description: - - credential names to export + - credential names, IDs, or named URLs to export type: list elements: str execution_environments: description: - - execution environment names to export + - execution environment names, IDs, or named URLs to export type: list elements: str notification_templates: description: - - notification template names to export + - notification template names, IDs, or named URLs to export type: list elements: str inventory_sources: description: - - inventory soruces to export + - inventory source name, ID, or named URLs to export type: list elements: str inventory: description: - - inventory names to export + - inventory names, IDs, or named URLs to export type: list elements: str projects: description: - - project names to export + - project names, IDs, or named URLs to export type: list elements: str job_templates: description: - - job template names to export + - job template names, IDs, or named URLs to export type: list elements: str workflow_job_templates: description: - - workflow names to export + - workflow names, IDs, or named URLs to export type: list elements: str applications: description: - - OAuth2 application names to export + - OAuth2 application names, IDs, or named URLs to export type: list elements: str schedules: description: - - schedule names to export + - schedule names, IDs, or named URLs to export type: list elements: str requirements: @@ -115,7 +115,7 @@ EXAMPLES = ''' - name: Export a job template named "My Template" and all Credentials export: job_templates: "My Template" - credential: 'all' + credentials: 'all' - name: Export a list of inventories export: @@ -154,12 +154,12 @@ def main(): # The exporter code currently works like the following: # Empty string == all assets of that type - # Non-Empty string = just one asset of that type (by name or ID) + # Non-Empty string = just a list of assets of that type (by name, ID, or named URL) # Asset type not present or None = skip asset type (unless everything is None, then export all) # Here we are going to setup a dict of values to export export_args = {} for resource in EXPORTABLE_RESOURCES: - if module.params.get('all') or module.params.get(resource) == 'all': + if module.params.get('all') or module.params.get(resource) == ['all']: # If we are exporting everything or we got the keyword "all" we pass in an empty string for this asset type export_args[resource] = '' else: diff --git a/ansible_collections/awx/awx/plugins/modules/group.py b/ansible_collections/awx/awx/plugins/modules/group.py index c91bf164d..32b7e4104 100644 --- a/ansible_collections/awx/awx/plugins/modules/group.py +++ b/ansible_collections/awx/awx/plugins/modules/group.py @@ -32,7 +32,7 @@ options: type: str inventory: description: - - Inventory the group should be made a member of. + - Inventory name, ID, or named URL that the group should be made a member of. required: True type: str variables: @@ -41,12 +41,12 @@ options: type: dict hosts: description: - - List of hosts that should be put in this group. + - List of host names, IDs, or named URLs that should be put in this group. type: list elements: str children: description: - - List of groups that should be nested inside in this group. + - List of groups names, IDs, or named URLs that should be nested inside in this group. type: list elements: str aliases: @@ -67,7 +67,7 @@ options: description: - Desired state of the resource. default: "present" - choices: ["present", "absent"] + choices: ["present", "absent", "exists"] type: str new_name: description: @@ -115,7 +115,7 @@ def main(): children=dict(type='list', elements='str', aliases=['groups']), preserve_existing_hosts=dict(type='bool', default=False), preserve_existing_children=dict(type='bool', default=False, aliases=['preserve_existing_groups']), - state=dict(choices=['present', 'absent'], default='present'), + state=dict(choices=['present', 'absent', 'exists'], default='present'), ) # Create a module for ourselves @@ -135,7 +135,7 @@ def main(): inventory_id = module.resolve_name_to_id('inventories', inventory) # Attempt to look up the object based on the provided name and inventory ID - group = module.get_one('groups', name_or_id=name, **{'data': {'inventory': inventory_id}}) + group = module.get_one('groups', name_or_id=name, check_exists=(state == 'exists'), **{'data': {'inventory': inventory_id}}) if state == 'absent': # If the state was absent we can let the module delete it if needed, the module will handle exiting from this @@ -170,9 +170,11 @@ def main(): id_list.append(sub_obj['id']) # Preserve existing objects if (preserve_existing_hosts and relationship == 'hosts') or (preserve_existing_children and relationship == 'children'): - preserve_existing_check = module.get_all_endpoint(group['related'][relationship]) - for sub_obj in preserve_existing_check['json']['results']: - id_list.append(sub_obj['id']) + if group: + preserve_existing_check = module.get_all_endpoint(group['related'][relationship]) + for sub_obj in preserve_existing_check['json']['results']: + if 'id' in sub_obj: + id_list.append(sub_obj['id']) if id_list: association_fields[relationship] = id_list diff --git a/ansible_collections/awx/awx/plugins/modules/host.py b/ansible_collections/awx/awx/plugins/modules/host.py index 21d063f39..9b8760b88 100644 --- a/ansible_collections/awx/awx/plugins/modules/host.py +++ b/ansible_collections/awx/awx/plugins/modules/host.py @@ -36,7 +36,7 @@ options: type: str inventory: description: - - Inventory the host should be made a member of. + - Inventory name, ID, or named URL the host should be made a member of. required: True type: str enabled: @@ -50,7 +50,7 @@ options: state: description: - Desired state of the resource. - choices: ["present", "absent"] + choices: ["present", "absent", "exists"] default: "present" type: str extends_documentation_fragment: awx.awx.auth @@ -83,7 +83,7 @@ def main(): inventory=dict(required=True), enabled=dict(type='bool'), variables=dict(type='dict'), - state=dict(choices=['present', 'absent'], default='present'), + state=dict(choices=['present', 'absent', 'exists'], default='present'), ) # Create a module for ourselves @@ -102,7 +102,7 @@ def main(): inventory_id = module.resolve_name_to_id('inventories', inventory) # Attempt to look up host based on the provided name and inventory ID - host = module.get_one('hosts', name_or_id=name, **{'data': {'inventory': inventory_id}}) + host = module.get_one('hosts', name_or_id=name, check_exists=(state == 'exists'), **{'data': {'inventory': inventory_id}}) if state == 'absent': # If the state was absent we can let the module delete it if needed, the module will handle exiting from this diff --git a/ansible_collections/awx/awx/plugins/modules/instance.py b/ansible_collections/awx/awx/plugins/modules/instance.py index 049dd976d..af7360687 100644 --- a/ansible_collections/awx/awx/plugins/modules/instance.py +++ b/ansible_collections/awx/awx/plugins/modules/instance.py @@ -47,6 +47,7 @@ options: - Role that this node plays in the mesh. choices: - execution + - hop required: False type: str node_state: @@ -62,6 +63,20 @@ options: - Port that Receptor will listen for incoming connections on. required: False type: int + peers: + description: + - List of peers to connect outbound to. Only configurable for hop and execution nodes. + - To remove all current peers, set value to an empty list, []. + - Each item is an ID or address of a receptor address. + - If item is address, it must be unique across all receptor addresses. + required: False + type: list + elements: str + peers_from_control_nodes: + description: + - If enabled, control plane nodes will automatically peer to this node. + required: False + type: bool extends_documentation_fragment: awx.awx.auth ''' @@ -70,12 +85,24 @@ EXAMPLES = ''' awx.awx.instance: hostname: my-instance.prod.example.com capacity_adjustment: 0.4 - listener_port: 31337 - name: Deprovision the instance awx.awx.instance: hostname: my-instance.prod.example.com node_state: deprovisioning + +- name: Create execution node + awx.awx.instance: + hostname: execution.example.com + node_type: execution + peers: + - 12 + - route.to.hop.example.com + +- name: Remove peers + awx.awx.instance: + hostname: execution.example.com + peers: ''' from ..module_utils.controller_api import ControllerAPIModule @@ -88,9 +115,11 @@ def main(): capacity_adjustment=dict(type='float'), enabled=dict(type='bool'), managed_by_policy=dict(type='bool'), - node_type=dict(type='str', choices=['execution']), + node_type=dict(type='str', choices=['execution', 'hop']), node_state=dict(type='str', choices=['deprovisioning', 'installed']), listener_port=dict(type='int'), + peers=dict(required=False, type='list', elements='str'), + peers_from_control_nodes=dict(required=False, type='bool'), ) # Create a module for ourselves @@ -104,10 +133,22 @@ def main(): node_type = module.params.get('node_type') node_state = module.params.get('node_state') listener_port = module.params.get('listener_port') - + peers = module.params.get('peers') + peers_from_control_nodes = module.params.get('peers_from_control_nodes') # Attempt to look up an existing item based on the provided data existing_item = module.get_one('instances', name_or_id=hostname) + # peer item can be an id or address + # if address, get the id + peers_ids = [] + if peers: + for p in peers: + if not p.isdigit(): + p_id = module.get_one('receptor_addresses', allow_none=False, data={'address': p}) + peers_ids.append(p_id['id']) + else: + peers_ids.append(p) + # Create the data that gets sent for create and update new_fields = {'hostname': hostname} if capacity_adjustment is not None: @@ -122,6 +163,10 @@ def main(): new_fields['node_state'] = node_state if listener_port is not None: new_fields['listener_port'] = listener_port + if peers is not None: + new_fields['peers'] = peers_ids + if peers_from_control_nodes is not None: + new_fields['peers_from_control_nodes'] = peers_from_control_nodes module.create_or_update_if_needed( existing_item, diff --git a/ansible_collections/awx/awx/plugins/modules/instance_group.py b/ansible_collections/awx/awx/plugins/modules/instance_group.py index dc993f8b5..32e6b192d 100644 --- a/ansible_collections/awx/awx/plugins/modules/instance_group.py +++ b/ansible_collections/awx/awx/plugins/modules/instance_group.py @@ -33,7 +33,7 @@ options: type: str credential: description: - - Credential to authenticate with Kubernetes or OpenShift. Must be of type "OpenShift or Kubernetes API Bearer Token". + - Credential name, ID, or named URL to authenticate with Kubernetes or OpenShift. Must be of type "OpenShift or Kubernetes API Bearer Token". required: False type: str is_container_group: @@ -74,14 +74,14 @@ options: type: str instances: description: - - The instances associated with this instance_group + - The instance names, IDs, or named URLs associated with this instance_group required: False type: list elements: str state: description: - Desired state of the resource. - choices: ["present", "absent"] + choices: ["present", "absent", "exists"] default: "present" type: str extends_documentation_fragment: awx.awx.auth @@ -107,7 +107,7 @@ def main(): policy_instance_list=dict(type='list', elements='str'), pod_spec_override=dict(), instances=dict(required=False, type="list", elements='str'), - state=dict(choices=['present', 'absent'], default='present'), + state=dict(choices=['present', 'absent', 'exists'], default='present'), ) # Create a module for ourselves @@ -128,7 +128,7 @@ def main(): state = module.params.get('state') # Attempt to look up an existing item based on the provided data - existing_item = module.get_one('instance_groups', name_or_id=name) + existing_item = module.get_one('instance_groups', name_or_id=name, check_exists=(state == 'exists')) if state == 'absent': # If the state was absent we can let the module delete it if needed, the module will handle exiting from this diff --git a/ansible_collections/awx/awx/plugins/modules/inventory.py b/ansible_collections/awx/awx/plugins/modules/inventory.py index 8e739b221..67ebe93f0 100644 --- a/ansible_collections/awx/awx/plugins/modules/inventory.py +++ b/ansible_collections/awx/awx/plugins/modules/inventory.py @@ -44,7 +44,7 @@ options: type: str organization: description: - - Organization the inventory belongs to. + - Organization name, ID, or named URL the inventory belongs to. required: True type: str variables: @@ -54,7 +54,7 @@ options: kind: description: - The kind field. Cannot be modified after created. - choices: ["", "smart"] + choices: ["", "smart", "constructed"] type: str host_filter: description: @@ -62,7 +62,12 @@ options: type: str instance_groups: description: - - list of Instance Groups for this Organization to run on. + - list of Instance Group names, IDs, or named URLs for this Organization to run on. + type: list + elements: str + input_inventories: + description: + - List of Inventory names, IDs, or named URLs to use as input for Constructed Inventory. type: list elements: str prevent_instance_group_fallback: @@ -73,7 +78,7 @@ options: description: - Desired state of the resource. default: "present" - choices: ["present", "absent"] + choices: ["present", "absent", "exists"] type: str extends_documentation_fragment: awx.awx.auth ''' @@ -95,6 +100,35 @@ EXAMPLES = ''' description: "Our Foo Cloud Servers" organization: Foo state: present + +# You can create and modify constructed inventories by creating an inventory +# of kind "constructed" and then editing the automatically generated inventory +# source for that inventory. +- name: Add constructed inventory with two existing input inventories + inventory: + name: My Constructed Inventory + organization: Default + kind: constructed + input_inventories: + - "West Datacenter" + - "East Datacenter" + +- name: Edit the constructed inventory source + inventory_source: + # The constructed inventory source will always be in the format: + # "Auto-created source for: <constructed inventory name>" + name: "Auto-created source for: My Constructed Inventory" + inventory: My Constructed Inventory + limit: host3,host4,host6 + source_vars: + plugin: constructed + strict: true + use_vars_plugins: true + groups: + shutdown: resolved_state == "shutdown" + shutdown_in_product_dev: resolved_state == "shutdown" and account_alias == "product_dev" + compose: + resolved_state: state | default("running") ''' @@ -111,11 +145,12 @@ def main(): description=dict(), organization=dict(required=True), variables=dict(type='dict'), - kind=dict(choices=['', 'smart']), + kind=dict(choices=['', 'smart', 'constructed']), host_filter=dict(), instance_groups=dict(type="list", elements='str'), prevent_instance_group_fallback=dict(type='bool'), - state=dict(choices=['present', 'absent'], default='present'), + state=dict(choices=['present', 'absent', 'exists'], default='present'), + input_inventories=dict(type='list', elements='str'), ) # Create a module for ourselves @@ -137,7 +172,7 @@ def main(): org_id = module.resolve_name_to_id('organizations', organization) # Attempt to look up inventory based on the provided name and org ID - inventory = module.get_one('inventories', name_or_id=name, **{'data': {'organization': org_id}}) + inventory = module.get_one('inventories', name_or_id=name, check_exists=(state == 'exists'), **{'data': {'organization': org_id}}) # Attempt to look up credential to copy based on the provided name if copy_from: @@ -181,6 +216,13 @@ def main(): if inventory and inventory['kind'] == '' and inventory_fields['kind'] == 'smart': module.fail_json(msg='You cannot turn a regular inventory into a "smart" inventory.') + if kind == 'constructed': + input_inventory_names = module.params.get('input_inventories') + if input_inventory_names is not None: + association_fields['input_inventories'] = [] + for item in input_inventory_names: + association_fields['input_inventories'].append(module.resolve_name_to_id('inventories', item)) + # If the state was present and we can let the module build or update the existing inventory, this will return on its own module.create_or_update_if_needed( inventory, diff --git a/ansible_collections/awx/awx/plugins/modules/inventory_source.py b/ansible_collections/awx/awx/plugins/modules/inventory_source.py index 1e6939df3..5f6c1781b 100644 --- a/ansible_collections/awx/awx/plugins/modules/inventory_source.py +++ b/ansible_collections/awx/awx/plugins/modules/inventory_source.py @@ -36,7 +36,7 @@ options: type: str inventory: description: - - Inventory the group should be made a member of. + - Inventory name, ID, or named URL the group should be made a member of. required: True type: str source: @@ -64,13 +64,17 @@ options: description: - If specified, AWX will only import hosts that match this regular expression. type: str + limit: + description: + - Enter host, group or pattern match + type: str credential: description: - - Credential to use for the source. + - Credential name, ID, or named URL to use for the source. type: str execution_environment: description: - - Execution Environment to use for the source. + - Execution Environment name, ID, or named URL to use for the source. type: str custom_virtualenv: description: @@ -103,7 +107,7 @@ options: type: int source_project: description: - - Project to use as source with scm option + - Project name, ID, or named URL to use as source with scm option type: str scm_branch: description: @@ -114,7 +118,7 @@ options: description: - Desired state of the resource. default: "present" - choices: ["present", "absent"] + choices: ["present", "absent", "exists"] type: str notification_templates_started: description: @@ -172,6 +176,7 @@ def main(): enabled_var=dict(), enabled_value=dict(), host_filter=dict(), + limit=dict(), credential=dict(), execution_environment=dict(), custom_virtualenv=dict(), @@ -187,7 +192,7 @@ def main(): notification_templates_started=dict(type="list", elements='str'), notification_templates_success=dict(type="list", elements='str'), notification_templates_error=dict(type="list", elements='str'), - state=dict(choices=['present', 'absent'], default='present'), + state=dict(choices=['present', 'absent', 'exists'], default='present'), ) # Create a module for ourselves @@ -214,6 +219,7 @@ def main(): inventory_source_object = module.get_one( 'inventory_sources', name_or_id=name, + check_exists=(state == 'exists'), **{ 'data': { 'inventory': inventory_object['id'], @@ -279,6 +285,7 @@ def main(): 'enabled_value', 'host_filter', 'scm_branch', + 'limit', ) # Layer in all remaining optional information diff --git a/ansible_collections/awx/awx/plugins/modules/inventory_source_update.py b/ansible_collections/awx/awx/plugins/modules/inventory_source_update.py index 5bd6cdfef..6e90e1ccf 100644 --- a/ansible_collections/awx/awx/plugins/modules/inventory_source_update.py +++ b/ansible_collections/awx/awx/plugins/modules/inventory_source_update.py @@ -35,7 +35,7 @@ options: type: str organization: description: - - Name of the inventory source's inventory's organization. + - Name, ID, or named URL of the inventory source's inventory's organization. type: str wait: description: diff --git a/ansible_collections/awx/awx/plugins/modules/job_launch.py b/ansible_collections/awx/awx/plugins/modules/job_launch.py index 9a76f3a8b..19500b73a 100644 --- a/ansible_collections/awx/awx/plugins/modules/job_launch.py +++ b/ansible_collections/awx/awx/plugins/modules/job_launch.py @@ -34,17 +34,17 @@ options: type: str inventory: description: - - Inventory to use for the job, only used if prompt for inventory is set. + - Inventory name, ID, or named URL to use for the job, only used if prompt for inventory is set. type: str organization: description: - - Organization the job template exists in. + - Organization name, ID, or named URL the job template exists in. - Used to help lookup the object, cannot be modified using this module. - If not provided, will lookup by name only, which does not work with duplicates. type: str credentials: description: - - Credential to use for job, only used if prompt for credential is set. + - Credential names, IDs, or named URLs to use for job, only used if prompt for credential is set. type: list aliases: ['credential'] elements: str @@ -88,7 +88,7 @@ options: type: dict execution_environment: description: - - Execution environment to use for the job, only used if prompt for execution environment is set. + - Execution environment name, ID, or named URL to use for the job, only used if prompt for execution environment is set. type: str forks: description: @@ -96,7 +96,7 @@ options: type: int instance_groups: description: - - Instance groups to use for the job, only used if prompt for instance groups is set. + - Instance group names, IDs, or named URLs to use for the job, only used if prompt for instance groups is set. type: list elements: str job_slice_count: @@ -151,7 +151,9 @@ EXAMPLES = ''' job_launch: job_template: "My Job Template" inventory: "My Inventory" - credential: "My Credential" + credentials: + - "My Credential" + - "suplementary cred" register: job - name: Wait for job max 120s job_wait: diff --git a/ansible_collections/awx/awx/plugins/modules/job_template.py b/ansible_collections/awx/awx/plugins/modules/job_template.py index 4508bc18d..81fe49512 100644 --- a/ansible_collections/awx/awx/plugins/modules/job_template.py +++ b/ansible_collections/awx/awx/plugins/modules/job_template.py @@ -49,11 +49,11 @@ options: type: str inventory: description: - - Name of the inventory to use for the job template. + - Name, ID, or named URL of the inventory to use for the job template. type: str organization: description: - - Organization the job template exists in. + - Organization name, ID, or named URL the job template exists in. - Used to help lookup the object, cannot be modified using this module. - The Organization is inferred from the associated project - If not provided, will lookup by name only, which does not work with duplicates. @@ -61,7 +61,7 @@ options: type: str project: description: - - Name of the project to use for the job template. + - Name, ID, or named URL of the project to use for the job template. type: str playbook: description: @@ -69,22 +69,22 @@ options: type: str credential: description: - - Name of the credential to use for the job template. + - Name, ID, or named URL of the credential to use for the job template. - Deprecated, use 'credentials'. type: str credentials: description: - - List of credentials to use for the job template. + - List of credential names, IDs, or named URLs to use for the job template. type: list elements: str vault_credential: description: - - Name of the vault credential to use for the job template. + - Name, ID, or named URL of the vault credential to use for the job template. - Deprecated, use 'credentials'. type: str execution_environment: description: - - Execution Environment to use for the job template. + - Execution Environment name, ID, or named URL to use for the job template. type: str custom_virtualenv: description: @@ -94,7 +94,7 @@ options: type: str instance_groups: description: - - list of Instance Groups for this Organization to run on. + - list of Instance Group names, IDs, or named URLs for this Organization to run on. type: list elements: str forks: @@ -108,7 +108,7 @@ options: verbosity: description: - Control the output level Ansible produces as the playbook runs. 0 - Normal, 1 - Verbose, 2 - More Verbose, 3 - Debug, 4 - Connection Debug. - choices: [0, 1, 2, 3, 4] + choices: [0, 1, 2, 3, 4, 5] type: int extra_vars: description: @@ -264,7 +264,6 @@ options: description: - Maximum time in seconds to wait for a job to finish (server-side). type: int - default: 0 job_slice_count: description: - The number of jobs to slice into at runtime. Will cause the Job Template to launch a workflow if value is greater than 1. @@ -295,7 +294,7 @@ options: description: - Desired state of the resource. default: "present" - choices: ["present", "absent"] + choices: ["present", "absent", "exists"] type: str notification_templates_started: description: @@ -337,6 +336,7 @@ EXAMPLES = ''' playbook: "ping.yml" credentials: - "Local" + - "2nd credential" state: "present" controller_config_file: "~/tower_cli.cfg" survey_enabled: yes @@ -404,7 +404,7 @@ def main(): instance_groups=dict(type="list", elements='str'), forks=dict(type='int'), limit=dict(), - verbosity=dict(type='int', choices=[0, 1, 2, 3, 4]), + verbosity=dict(type='int', choices=[0, 1, 2, 3, 4, 5]), extra_vars=dict(type='dict'), job_tags=dict(), force_handlers=dict(type='bool', aliases=['force_handlers_enabled']), @@ -443,7 +443,7 @@ def main(): notification_templates_success=dict(type="list", elements='str'), notification_templates_error=dict(type="list", elements='str'), prevent_instance_group_fallback=dict(type="bool"), - state=dict(choices=['present', 'absent'], default='present'), + state=dict(choices=['present', 'absent', 'exists'], default='present'), ) # Create a module for ourselves @@ -483,7 +483,7 @@ def main(): new_fields['execution_environment'] = module.resolve_name_to_id('execution_environments', ee) # Attempt to look up an existing item based on the provided data - existing_item = module.get_one('job_templates', name_or_id=name, **{'data': search_fields}) + existing_item = module.get_one('job_templates', name_or_id=name, check_exists=(state == 'exists'), **{'data': search_fields}) # Attempt to look up credential to copy based on the provided name if copy_from: diff --git a/ansible_collections/awx/awx/plugins/modules/label.py b/ansible_collections/awx/awx/plugins/modules/label.py index b17d58bee..230f9f470 100644 --- a/ansible_collections/awx/awx/plugins/modules/label.py +++ b/ansible_collections/awx/awx/plugins/modules/label.py @@ -34,14 +34,14 @@ options: type: str organization: description: - - Organization this label belongs to. + - Organization name, ID, or named URL this label belongs to. required: True type: str state: description: - Desired state of the resource. default: "present" - choices: ["present"] + choices: ["present", "exists"] type: str extends_documentation_fragment: awx.awx.auth ''' @@ -62,7 +62,7 @@ def main(): name=dict(required=True), new_name=dict(), organization=dict(required=True), - state=dict(choices=['present'], default='present'), + state=dict(choices=['present', 'exists'], default='present'), ) # Create a module for ourselves @@ -72,6 +72,7 @@ def main(): name = module.params.get('name') new_name = module.params.get("new_name") organization = module.params.get('organization') + state = module.params.get("state") # Attempt to look up the related items the user specified (these will fail the module if not found) organization_id = None @@ -82,6 +83,7 @@ def main(): existing_item = module.get_one( 'labels', name_or_id=name, + check_exists=(state == 'exists'), **{ 'data': { 'organization': organization_id, diff --git a/ansible_collections/awx/awx/plugins/modules/license.py b/ansible_collections/awx/awx/plugins/modules/license.py index ed9b93727..06f02a821 100644 --- a/ansible_collections/awx/awx/plugins/modules/license.py +++ b/ansible_collections/awx/awx/plugins/modules/license.py @@ -52,7 +52,12 @@ EXAMPLES = ''' license: manifest: "/tmp/my_manifest.zip" -- name: Attach to a pool +- name: Use the subscriptions module to fetch subscriptions from Red Hat or Red Hat Satellite + subscriptions: + username: "my_satellite_username" + password: "my_satellite_password" + +- name: Attach to a pool (requires fetching subscriptions at least once before) license: pool_id: 123456 diff --git a/ansible_collections/awx/awx/plugins/modules/notification_template.py b/ansible_collections/awx/awx/plugins/modules/notification_template.py index 6da77c659..bb1df60d3 100644 --- a/ansible_collections/awx/awx/plugins/modules/notification_template.py +++ b/ansible_collections/awx/awx/plugins/modules/notification_template.py @@ -44,7 +44,7 @@ options: type: str organization: description: - - The organization the notification belongs to. + - The organization name, ID, or named URL the notification belongs to. type: str notification_type: description: @@ -97,7 +97,7 @@ options: description: - Desired state of the resource. default: "present" - choices: ["present", "absent"] + choices: ["present", "absent", "exists"] type: str extends_documentation_fragment: awx.awx.auth ''' @@ -222,7 +222,7 @@ def main(): notification_type=dict(choices=['email', 'grafana', 'irc', 'mattermost', 'pagerduty', 'rocketchat', 'slack', 'twilio', 'webhook']), notification_configuration=dict(type='dict'), messages=dict(type='dict'), - state=dict(choices=['present', 'absent'], default='present'), + state=dict(choices=['present', 'absent', 'exists'], default='present'), ) # Create a module for ourselves @@ -248,6 +248,7 @@ def main(): existing_item = module.get_one( 'notification_templates', name_or_id=name, + check_exists=(state == 'exists'), **{ 'data': { 'organization': organization_id, diff --git a/ansible_collections/awx/awx/plugins/modules/organization.py b/ansible_collections/awx/awx/plugins/modules/organization.py index de78eb228..6fc406b92 100644 --- a/ansible_collections/awx/awx/plugins/modules/organization.py +++ b/ansible_collections/awx/awx/plugins/modules/organization.py @@ -36,7 +36,7 @@ options: type: str default_environment: description: - - Default Execution Environment to use for jobs owned by the Organization. + - Default Execution Environment name, ID, or named URL to use for jobs owned by the Organization. type: str custom_virtualenv: description: @@ -52,11 +52,11 @@ options: description: - Desired state of the resource. default: "present" - choices: ["present", "absent"] + choices: ["present", "absent", "exists"] type: str instance_groups: description: - - list of Instance Groups for this Organization to run on. + - list of Instance Group names, IDs, or named URLs for this Organization to run on. type: list elements: str notification_templates_started: @@ -81,7 +81,7 @@ options: elements: str galaxy_credentials: description: - - list of Ansible Galaxy credentials to associate to the organization + - list of Ansible Galaxy credential names, IDs, or named URLs to associate to the organization type: list elements: str extends_documentation_fragment: awx.awx.auth @@ -130,7 +130,7 @@ def main(): notification_templates_error=dict(type="list", elements='str'), notification_templates_approvals=dict(type="list", elements='str'), galaxy_credentials=dict(type="list", elements='str'), - state=dict(choices=['present', 'absent'], default='present'), + state=dict(choices=['present', 'absent', 'exists'], default='present'), ) # Create a module for ourselves @@ -146,7 +146,7 @@ def main(): state = module.params.get('state') # Attempt to look up organization based on the provided name - organization = module.get_one('organizations', name_or_id=name) + organization = module.get_one('organizations', name_or_id=name, check_exists=(state == 'exists')) if state == 'absent': # If the state was absent we can let the module delete it if needed, the module will handle exiting from this diff --git a/ansible_collections/awx/awx/plugins/modules/project.py b/ansible_collections/awx/awx/plugins/modules/project.py index 877340a11..2621e57ab 100644 --- a/ansible_collections/awx/awx/plugins/modules/project.py +++ b/ansible_collections/awx/awx/plugins/modules/project.py @@ -65,7 +65,7 @@ options: type: str credential: description: - - Name of the credential to use with this SCM resource. + - Name, ID, or named URL of the credential to use with this SCM resource. type: str aliases: - scm_credential @@ -83,7 +83,7 @@ options: type: bool scm_update_on_launch: description: - - Before an update to the local repository before launching a job with this project. + - Perform an update to the local repository before launching a job with this project. type: bool scm_update_cache_timeout: description: @@ -106,7 +106,7 @@ options: - job_timeout default_environment: description: - - Default Execution Environment to use for jobs relating to the project. + - Default Execution Environment name, ID, or named URL to use for jobs relating to the project. type: str custom_virtualenv: description: @@ -116,13 +116,13 @@ options: type: str organization: description: - - Name of organization for project. + - Name, ID, or named URL of organization for the project. type: str state: description: - Desired state of the resource. default: "present" - choices: ["present", "absent"] + choices: ["present", "absent", "exists"] type: str wait: description: @@ -162,7 +162,7 @@ options: type: float signature_validation_credential: description: - - Name of the credential to use for signature validation. + - Name, ID, or named URL of the credential to use for signature validation. - If signature validation credential is provided, signature validation will be enabled. type: str @@ -272,7 +272,7 @@ def main(): notification_templates_started=dict(type="list", elements='str'), notification_templates_success=dict(type="list", elements='str'), notification_templates_error=dict(type="list", elements='str'), - state=dict(choices=['present', 'absent'], default='present'), + state=dict(choices=['present', 'absent', 'exists'], default='present'), wait=dict(type='bool', default=True), update_project=dict(default=False, type='bool'), interval=dict(default=2.0, type='float'), @@ -313,7 +313,7 @@ def main(): lookup_data['organization'] = org_id # Attempt to look up project based on the provided name and org ID - project = module.get_one('projects', name_or_id=name, data=lookup_data) + project = module.get_one('projects', name_or_id=name, check_exists=(state == 'exists'), data=lookup_data) # Attempt to look up credential to copy based on the provided name if copy_from: diff --git a/ansible_collections/awx/awx/plugins/modules/project_update.py b/ansible_collections/awx/awx/plugins/modules/project_update.py index 6cbcd39b6..d6d712f8f 100644 --- a/ansible_collections/awx/awx/plugins/modules/project_update.py +++ b/ansible_collections/awx/awx/plugins/modules/project_update.py @@ -27,7 +27,7 @@ options: - project organization: description: - - Organization the project exists in. + - Organization name, ID, or named URL the project exists in. - Used to help lookup the object, cannot be modified using this module. - If not provided, will lookup by name only, which does not work with duplicates. type: str @@ -116,8 +116,7 @@ def main(): if result['status_code'] == 405: module.fail_json( - msg="Unable to trigger a project update because the project scm_type ({0}) does not support it.".format(project['scm_type']), - response=result + msg="Unable to trigger a project update because the project scm_type ({0}) does not support it.".format(project['scm_type']), response=result ) elif result['status_code'] != 202: module.fail_json(msg="Failed to update project, see response for details", response=result) diff --git a/ansible_collections/awx/awx/plugins/modules/role.py b/ansible_collections/awx/awx/plugins/modules/role.py index 51ed56439..40463746b 100644 --- a/ansible_collections/awx/awx/plugins/modules/role.py +++ b/ansible_collections/awx/awx/plugins/modules/role.py @@ -23,12 +23,24 @@ description: options: user: description: - - User that receives the permissions specified by the role. + - User name, ID, or named URL that receives the permissions specified by the role. + - Deprecated, use 'users'. type: str + users: + description: + - User names, IDs, or named URLs that receive the permissions specified by the role. + type: list + elements: str team: description: - - Team that receives the permissions specified by the role. + - Team name, ID, or named URL that receives the permissions specified by the role. + - Deprecated, use 'teams'. type: str + teams: + description: + - Team names, IDs, or named URLs that receive the permissions specified by the role. + type: list + elements: str role: description: - The role type to grant/revoke. @@ -38,82 +50,87 @@ options: type: str target_team: description: - - Team that the role acts on. + - Team name, ID, or named URL that the role acts on. - For example, make someone a member or an admin of a team. - Members of a team implicitly receive the permissions that the team has. - Deprecated, use 'target_teams'. type: str target_teams: description: - - Team that the role acts on. + - Team names, IDs, or named URLs that the role acts on. - For example, make someone a member or an admin of a team. - Members of a team implicitly receive the permissions that the team has. type: list elements: str inventory: description: - - Inventory the role acts on. + - Inventory name, ID, or named URL the role acts on. - Deprecated, use 'inventories'. type: str inventories: description: - - Inventory the role acts on. + - Inventory names, IDs, or named URLs the role acts on. type: list elements: str job_template: description: - - The job template the role acts on. + - The job template name, ID, or named URL the role acts on. - Deprecated, use 'job_templates'. type: str job_templates: description: - - The job template the role acts on. + - The job template names, IDs, or named URLs the role acts on. type: list elements: str workflow: description: - - The workflow job template the role acts on. + - The workflow job template name, ID, or named URL the role acts on. - Deprecated, use 'workflows'. type: str workflows: description: - - The workflow job template the role acts on. + - The workflow job template names, IDs, or named URLs the role acts on. type: list elements: str credential: description: - - Credential the role acts on. + - Credential name, ID, or named URL the role acts on. - Deprecated, use 'credentials'. type: str credentials: description: - - Credential the role acts on. + - Credential names, IDs, or named URLs the role acts on. type: list elements: str organization: description: - - Organization the role acts on. + - Organization name, ID, or named URL the role acts on. - Deprecated, use 'organizations'. type: str organizations: description: - - Organization the role acts on. + - Organization names, IDs, or named URLs the role acts on. type: list elements: str lookup_organization: description: - - Organization the inventories, job templates, projects, or workflows the items exists in. + - Organization name, ID, or named URL the inventories, job templates, projects, or workflows the items exists in. - Used to help lookup the object, for organization roles see organization. - If not provided, will lookup by name only, which does not work with duplicates. type: str project: description: - - Project the role acts on. + - Project name, ID, or named URL the role acts on. - Deprecated, use 'projects'. type: str projects: description: - - Project the role acts on. + - Project names, IDs, or named URLs the role acts on. + type: list + elements: str + instance_groups: + description: + - Instance Group names, IDs, or named URLs the role acts on. type: list elements: str state: @@ -156,7 +173,9 @@ def main(): argument_spec = dict( user=dict(), + users=dict(type='list', elements='str'), team=dict(), + teams=dict(type='list', elements='str'), role=dict( choices=[ "admin", @@ -193,6 +212,7 @@ def main(): lookup_organization=dict(), project=dict(), projects=dict(type='list', elements='str'), + instance_groups=dict(type='list', elements='str'), state=dict(choices=['present', 'absent'], default='present'), ) @@ -213,9 +233,10 @@ def main(): 'projects': 'project', 'target_teams': 'target_team', 'workflows': 'workflow', + 'users': 'user', + 'teams': 'team', + 'instance_groups': 'instance_group', } - # Singular parameters - resource_param_keys = ('user', 'team', 'lookup_organization') resources = {} for resource_group, old_name in resource_list_param_keys.items(): @@ -223,14 +244,13 @@ def main(): resources.setdefault(resource_group, []).extend(module.params.get(resource_group)) if module.params.get(old_name) is not None: resources.setdefault(resource_group, []).append(module.params.get(old_name)) - for resource_group in resource_param_keys: - if module.params.get(resource_group) is not None: - resources[resource_group] = module.params.get(resource_group) - # Change workflows and target_teams key to its endpoint name. + if module.params.get('lookup_organization') is not None: + resources['lookup_organization'] = module.params.get('lookup_organization') + if module.params.get('instance_groups') is not None: + resources['instance_groups'] = module.params.get('instance_groups') + # Change workflows to its endpoint name. if 'workflows' in resources: resources['workflow_job_templates'] = resources.pop('workflows') - if 'target_teams' in resources: - resources['teams'] = resources.pop('target_teams') # Set lookup data to use lookup_data = {} @@ -242,43 +262,36 @@ def main(): # separate actors from resources actor_data = {} missing_items = [] - for key in ('user', 'team'): - if key in resources: - if key == 'user': - lookup_data_populated = {} - else: - lookup_data_populated = lookup_data - # Attempt to look up project based on the provided name or ID and lookup data - data = module.get_one('{0}s'.format(key), name_or_id=resources[key], data=lookup_data_populated) - if data is None: - module.fail_json( - msg='Unable to find {0} with name: {1}'.format(key, resources[key]), changed=False - ) - else: - actor_data[key] = module.get_one('{0}s'.format(key), name_or_id=resources[key], data=lookup_data_populated) - resources.pop(key) # Lookup Resources resource_data = {} for key, value in resources.items(): for resource in value: - # Attempt to look up project based on the provided name or ID and lookup data - if key in resources: - if key == 'organizations': - lookup_data_populated = {} - else: - lookup_data_populated = lookup_data - data = module.get_one(key, name_or_id=resource, data=lookup_data_populated) + # Attempt to look up project based on the provided name, ID, or named URL and lookup data + lookup_key = key + if key == 'organizations' or key == 'users': + lookup_data_populated = {} + else: + lookup_data_populated = lookup_data + if key == 'target_teams': + lookup_key = 'teams' + data = module.get_one(lookup_key, name_or_id=resource, data=lookup_data_populated) if data is None: missing_items.append(resource) else: - resource_data.setdefault(key, []).append(data) + if key == 'users' or key == 'teams': + actor_data.setdefault(key, []).append(data) + elif key == 'target_teams': + resource_data.setdefault('teams', []).append(data) + else: + resource_data.setdefault(key, []).append(data) if len(missing_items) > 0: module.fail_json( msg='There were {0} missing items, missing items: {1}'.format(len(missing_items), missing_items), changed=False ) + # build association agenda associations = {} - for actor_type, actor in actor_data.items(): + for actor_type, actors in actor_data.items(): for key, value in resource_data.items(): for resource in value: resource_roles = resource['summary_fields']['object_roles'] @@ -288,9 +301,10 @@ def main(): msg='Resource {0} has no role {1}, available roles: {2}'.format(resource['url'], role_field, available_roles), changed=False ) role_data = resource_roles[role_field] - endpoint = '/roles/{0}/{1}/'.format(role_data['id'], module.param_to_endpoint(actor_type)) + endpoint = '/roles/{0}/{1}/'.format(role_data['id'], actor_type) associations.setdefault(endpoint, []) - associations[endpoint].append(actor['id']) + for actor in actors: + associations[endpoint].append(actor['id']) # perform associations for association_endpoint, new_association_list in associations.items(): diff --git a/ansible_collections/awx/awx/plugins/modules/schedule.py b/ansible_collections/awx/awx/plugins/modules/schedule.py index 5bf821025..4a6583629 100644 --- a/ansible_collections/awx/awx/plugins/modules/schedule.py +++ b/ansible_collections/awx/awx/plugins/modules/schedule.py @@ -44,7 +44,7 @@ options: type: str execution_environment: description: - - Execution Environment applied as a prompt, assuming jot template prompts for execution environment + - Execution Environment name, ID, or named URL applied as a prompt, assuming job template prompts for execution environment type: str extra_data: description: @@ -57,12 +57,12 @@ options: type: int instance_groups: description: - - List of Instance Groups applied as a prompt, assuming job template prompts for instance groups + - List of Instance Group names, IDs, or named URLs applied as a prompt, assuming job template prompts for instance groups type: list elements: str inventory: description: - - Inventory applied as a prompt, assuming job template prompts for inventory + - Inventory name, ID, or named URL applied as a prompt, assuming job template prompts for inventory required: False type: str job_slice_count: @@ -76,7 +76,7 @@ options: elements: str credentials: description: - - List of credentials applied as a prompt, assuming job template prompts for credentials + - List of credential names, IDs, or named URLs applied as a prompt, assuming job template prompts for credentials type: list elements: str scm_branch: @@ -130,12 +130,12 @@ options: - 5 unified_job_template: description: - - Name of unified job template to schedule. Used to look up an already existing schedule. + - Name, ID, or named URL of unified job template to schedule. Used to look up an already existing schedule. required: False type: str organization: description: - - The organization the unified job template exists in. + - The organization name, ID, or named URL the unified job template exists in. - Used for looking up the unified job template, not a direct model field. type: str enabled: @@ -146,7 +146,7 @@ options: state: description: - Desired state of the resource. - choices: ["present", "absent"] + choices: ["present", "absent", "exists"] default: "present" type: str extends_documentation_fragment: awx.awx.auth @@ -220,7 +220,7 @@ def main(): unified_job_template=dict(), organization=dict(), enabled=dict(type='bool'), - state=dict(choices=['present', 'absent'], default='present'), + state=dict(choices=['present', 'absent', 'exists'], default='present'), ) # Create a module for ourselves @@ -265,8 +265,33 @@ def main(): search_fields['name'] = unified_job_template unified_job_template_id = module.get_one('unified_job_templates', **{'data': search_fields})['id'] sched_search_fields['unified_job_template'] = unified_job_template_id + # Attempt to look up an existing item based on the provided data - existing_item = module.get_one('schedules', name_or_id=name, **{'data': sched_search_fields}) + existing_item = module.get_one('schedules', name_or_id=name, check_exists=(state == 'exists'), **{'data': sched_search_fields}) + + if state == 'absent': + # If the state was absent we can let the module delete it if needed, the module will handle exiting from this + module.delete_if_needed(existing_item) + + # We need to clear out the name from the search fields so we can use name_or_id in the following searches + if 'name' in search_fields: + del search_fields['name'] + + # Create the data that gets sent for create and update + new_fields = {} + if execution_environment is not None: + if execution_environment == '': + new_fields['execution_environment'] = '' + else: + ee = module.get_one('execution_environments', name_or_id=execution_environment, **{'data': search_fields}) + if ee is None: + ee2 = module.get_one('execution_environments', name_or_id=execution_environment) + if ee2 is None or ee2['organization'] is not None: + module.fail_json(msg='could not find execution_environment entry with name {0}'.format(execution_environment)) + else: + new_fields['execution_environment'] = ee2['id'] + else: + new_fields['execution_environment'] = ee['id'] association_fields = {} @@ -275,9 +300,9 @@ def main(): for item in credentials: association_fields['credentials'].append(module.resolve_name_to_id('credentials', item)) - # We need to clear out the name from the search fields so we can use name_or_id in the following searches - if 'name' in search_fields: - del search_fields['name'] + # We need to clear out the organization from the search fields the searches for labels and instance_groups doesnt support it and won't be needed anymore + if 'organization' in search_fields: + del search_fields['organization'] if labels is not None: association_fields['labels'] = [] @@ -297,8 +322,6 @@ def main(): else: association_fields['instance_groups'].append(instance_group_id['id']) - # Create the data that gets sent for create and update - new_fields = {} if rrule is not None: new_fields['rrule'] = rrule new_fields['name'] = new_name if new_name else (module.get_item_name(existing_item) if existing_item else name) @@ -333,28 +356,14 @@ def main(): if timeout is not None: new_fields['timeout'] = timeout - if execution_environment is not None: - if execution_environment == '': - new_fields['execution_environment'] = '' - else: - ee = module.get_one('execution_environments', name_or_id=execution_environment, **{'data': search_fields}) - if ee is None: - module.fail_json(msg='could not find execution_environment entry with name {0}'.format(execution_environment)) - else: - new_fields['execution_environment'] = ee['id'] - - if state == 'absent': - # If the state was absent we can let the module delete it if needed, the module will handle exiting from this - module.delete_if_needed(existing_item) - elif state == 'present': - # If the state was present and we can let the module build or update the existing item, this will return on its own - module.create_or_update_if_needed( - existing_item, - new_fields, - endpoint='schedules', - item_type='schedule', - associations=association_fields, - ) + # If the state was present and we can let the module build or update the existing item, this will return on its own + module.create_or_update_if_needed( + existing_item, + new_fields, + endpoint='schedules', + item_type='schedule', + associations=association_fields, + ) if __name__ == '__main__': diff --git a/ansible_collections/awx/awx/plugins/modules/settings.py b/ansible_collections/awx/awx/plugins/modules/settings.py index 56c0b94e5..c911f77fc 100644 --- a/ansible_collections/awx/awx/plugins/modules/settings.py +++ b/ansible_collections/awx/awx/plugins/modules/settings.py @@ -89,7 +89,7 @@ def coerce_type(module, value): if not HAS_YAML: module.fail_json(msg="yaml is not installed, try 'pip install pyyaml'") return yaml.safe_load(value) - elif value.lower in ('true', 'false', 't', 'f'): + elif value.lower() in ('true', 'false', 't', 'f'): return {'t': True, 'f': False}[value[0].lower()] try: return int(value) diff --git a/ansible_collections/awx/awx/plugins/modules/team.py b/ansible_collections/awx/awx/plugins/modules/team.py index 5482b4c85..6507e8ac0 100644 --- a/ansible_collections/awx/awx/plugins/modules/team.py +++ b/ansible_collections/awx/awx/plugins/modules/team.py @@ -36,13 +36,13 @@ options: type: str organization: description: - - Organization the team should be made a member of. + - Organization name, ID, or named URL the team should be made a member of. required: True type: str state: description: - Desired state of the resource. - choices: ["present", "absent"] + choices: ["present", "absent", "exists"] default: "present" type: str extends_documentation_fragment: awx.awx.auth @@ -69,7 +69,7 @@ def main(): new_name=dict(), description=dict(), organization=dict(required=True), - state=dict(choices=['present', 'absent'], default='present'), + state=dict(choices=['present', 'absent', 'exists'], default='present'), ) # Create a module for ourselves @@ -86,7 +86,7 @@ def main(): org_id = module.resolve_name_to_id('organizations', organization) # Attempt to look up team based on the provided name and org ID - team = module.get_one('teams', name_or_id=name, **{'data': {'organization': org_id}}) + team = module.get_one('teams', name_or_id=name, check_exists=(state == 'exists'), **{'data': {'organization': org_id}}) if state == 'absent': # If the state was absent we can let the module delete it if needed, the module will handle exiting from this diff --git a/ansible_collections/awx/awx/plugins/modules/token.py b/ansible_collections/awx/awx/plugins/modules/token.py index c9ed84f67..b6e13cd02 100644 --- a/ansible_collections/awx/awx/plugins/modules/token.py +++ b/ansible_collections/awx/awx/plugins/modules/token.py @@ -37,7 +37,7 @@ options: type: str application: description: - - The application tied to this token. + - The application name, ID, or named URL tied to this token. required: False type: str scope: diff --git a/ansible_collections/awx/awx/plugins/modules/user.py b/ansible_collections/awx/awx/plugins/modules/user.py index 49a6f216a..be1bb61de 100644 --- a/ansible_collections/awx/awx/plugins/modules/user.py +++ b/ansible_collections/awx/awx/plugins/modules/user.py @@ -44,7 +44,7 @@ options: type: str organization: description: - - The user will be created as a member of that organization (needed for organization admins to create new organization users). + - The user will be created as a member of that organization name, ID, or named URL (needed for organization admins to create new organization users). type: str is_superuser: description: @@ -69,7 +69,7 @@ options: state: description: - Desired state of the resource. - choices: ["present", "absent"] + choices: ["present", "absent", "exists"] default: "present" type: str extends_documentation_fragment: awx.awx.auth @@ -137,7 +137,7 @@ def main(): password=dict(no_log=True), update_secrets=dict(type='bool', default=True, no_log=False), organization=dict(), - state=dict(choices=['present', 'absent'], default='present'), + state=dict(choices=['present', 'absent', 'exists'], default='present'), ) # Create a module for ourselves @@ -158,7 +158,7 @@ def main(): # Attempt to look up the related items the user specified (these will fail the module if not found) # Attempt to look up an existing item based on the provided data - existing_item = module.get_one('users', name_or_id=username) + existing_item = module.get_one('users', name_or_id=username, check_exists=(state == 'exists')) if state == 'absent': # If the state was absent we can let the module delete it if needed, the module will handle exiting from this diff --git a/ansible_collections/awx/awx/plugins/modules/workflow_job_template.py b/ansible_collections/awx/awx/plugins/modules/workflow_job_template.py index 19954877b..f5ac994aa 100644 --- a/ansible_collections/awx/awx/plugins/modules/workflow_job_template.py +++ b/ansible_collections/awx/awx/plugins/modules/workflow_job_template.py @@ -58,7 +58,7 @@ options: - ask_tags organization: description: - - Organization the workflow job template exists in. + - Organization name, ID, or named URL the workflow job template exists in. - Used to help lookup the object, cannot be modified using this module. - If not provided, will lookup by name only, which does not work with duplicates. type: str @@ -72,7 +72,7 @@ options: type: bool inventory: description: - - Inventory applied as a prompt, assuming job template prompts for inventory + - Name, ID, or named URL of inventory applied as a prompt, assuming job template prompts for inventory type: str limit: description: @@ -144,6 +144,7 @@ options: choices: - present - absent + - exists default: "present" type: str notification_templates_started: @@ -461,7 +462,9 @@ EXAMPLES = ''' failure_nodes: - identifier: node201 always_nodes: [] - credentials: [] + credentials: + - local_cred + - suplementary cred - identifier: node201 unified_job_template: organization: @@ -514,68 +517,63 @@ EXAMPLES = ''' workflow_nodes: - identifier: node101 unified_job_template: - name: example-project + name: example-inventory inventory: organization: name: Default type: inventory_source related: - success_nodes: [] failure_nodes: - identifier: node201 - always_nodes: [] - credentials: [] - - identifier: node201 - unified_job_template: - organization: - name: Default - name: job template 1 - type: job_template - credentials: [] - related: - success_nodes: - - identifier: node301 - failure_nodes: [] - always_nodes: [] - credentials: [] - - identifier: node202 + - identifier: node102 unified_job_template: organization: name: Default name: example-project type: project related: - success_nodes: [] - failure_nodes: [] - always_nodes: [] - credentials: [] - - identifier: node301 - all_parents_must_converge: false + success_nodes: + - identifier: node201 + - identifier: node201 unified_job_template: organization: name: Default - name: job template 2 + name: example-job template type: job_template - execution_environment: - name: My EE inventory: - name: Test inventory + name: Demo Inventory organization: name: Default related: + success_nodes: + - identifier: node401 + failure_nodes: + - identifier: node301 + always_nodes: [] credentials: - name: cyberark organization: name: Default instance_groups: - name: SunCavanaugh Cloud - - name: default labels: - name: Custom Label - - name: Another Custom Label organization: name: Default - register: result + - all_parents_must_converge: false + identifier: node301 + unified_job_template: + description: Approval node for example + timeout: 900 + type: workflow_approval + name: Approval Node for Demo + related: + success_nodes: + - identifier: node401 + - identifier: node401 + unified_job_template: + name: Cleanup Activity Stream + type: system_job_template ''' @@ -665,8 +663,7 @@ def create_workflow_nodes(module, response, workflow_nodes, workflow_id): inv_lookup_data = {} if 'organization' in workflow_node['inventory']: inv_lookup_data['organization'] = module.resolve_name_to_id('organizations', workflow_node['inventory']['organization']['name']) - workflow_node_fields['inventory'] = module.get_one( - 'inventories', name_or_id=workflow_node['inventory']['name'], data=inv_lookup_data)['id'] + workflow_node_fields['inventory'] = module.get_one('inventories', name_or_id=workflow_node['inventory']['name'], data=inv_lookup_data)['id'] else: workflow_node_fields['inventory'] = module.get_one('inventories', name_or_id=workflow_node['inventory'])['id'] @@ -841,7 +838,7 @@ def main(): notification_templates_approvals=dict(type="list", elements='str'), workflow_nodes=dict(type='list', elements='dict', aliases=['schema']), destroy_current_nodes=dict(type='bool', default=False, aliases=['destroy_current_schema']), - state=dict(choices=['present', 'absent'], default='present'), + state=dict(choices=['present', 'absent', 'exists'], default='present'), ) # Create a module for ourselves @@ -869,7 +866,7 @@ def main(): search_fields['organization'] = new_fields['organization'] = organization_id # Attempt to look up an existing item based on the provided data - existing_item = module.get_one('workflow_job_templates', name_or_id=name, **{'data': search_fields}) + existing_item = module.get_one('workflow_job_templates', name_or_id=name, check_exists=(state == 'exists'), **{'data': search_fields}) # Attempt to look up credential to copy based on the provided name if copy_from: @@ -989,6 +986,7 @@ def main(): # Destroy current nodes if selected. if destroy_current_nodes: destroy_workflow_nodes(module, response, workflow_job_template_id) + module.json_output['changed'] = True # Work thorugh and lookup value for schema fields if workflow_nodes: diff --git a/ansible_collections/awx/awx/plugins/modules/workflow_job_template_node.py b/ansible_collections/awx/awx/plugins/modules/workflow_job_template_node.py index a59cd33f9..71ca1c140 100644 --- a/ansible_collections/awx/awx/plugins/modules/workflow_job_template_node.py +++ b/ansible_collections/awx/awx/plugins/modules/workflow_job_template_node.py @@ -31,7 +31,7 @@ options: type: dict inventory: description: - - Inventory applied as a prompt, if job template prompts for inventory + - Name, ID, or named URL of the Inventory applied as a prompt, if job template prompts for inventory type: str scm_branch: description: @@ -73,7 +73,7 @@ options: - '5' workflow_job_template: description: - - The workflow job template the node exists in. + - The workflow job template name, ID, or named URL the node exists in. - Used for looking up the node, cannot be modified after creation. required: True type: str @@ -81,7 +81,7 @@ options: - workflow organization: description: - - The organization of the workflow job template the node exists in. + - The organization name, ID, or named URL of the workflow job template the node exists in. - Used for looking up the workflow, not a direct model field. type: str unified_job_template: @@ -93,7 +93,7 @@ options: type: str lookup_organization: description: - - Organization the inventories, job template, project, inventory source the unified_job_template exists in. + - Organization name, ID, or named URL the inventories, job template, project, inventory source the unified_job_template exists in. - If not provided, will lookup by name only, which does not work with duplicates. type: str approval_node: @@ -145,14 +145,14 @@ options: elements: str credentials: description: - - Credentials to be applied to job as launch-time prompts. - - List of credential names. + - Credential names, IDs, or named URLs to be applied to job as launch-time prompts. + - List of credential names, IDs, or named URLs. - Uniqueness is not handled rigorously. type: list elements: str execution_environment: description: - - Execution Environment applied as a prompt, assuming jot template prompts for execution environment + - Execution Environment name, ID, or named URL applied as a prompt, assuming job template prompts for execution environment type: str forks: description: @@ -160,7 +160,7 @@ options: type: int instance_groups: description: - - List of Instance Groups applied as a prompt, assuming job template prompts for instance groups + - List of Instance Group names, IDs, or named URLs applied as a prompt, assuming job template prompts for instance groups type: list elements: str job_slice_count: @@ -179,7 +179,7 @@ options: state: description: - Desired state of the resource. - choices: ["present", "absent"] + choices: ["present", "absent", "exists"] default: "present" type: str extends_documentation_fragment: awx.awx.auth @@ -285,7 +285,7 @@ def main(): job_slice_count=dict(type='int'), labels=dict(type='list', elements='str'), timeout=dict(type='int'), - state=dict(choices=['present', 'absent'], default='present'), + state=dict(choices=['present', 'absent', 'exists'], default='present'), ) mutually_exclusive = [("unified_job_template", "approval_node")] required_if = [ @@ -327,7 +327,7 @@ def main(): search_fields['workflow_job_template'] = new_fields['workflow_job_template'] = workflow_job_template_id # Attempt to look up an existing item based on the provided data - existing_item = module.get_one('workflow_job_template_nodes', **{'data': search_fields}) + existing_item = module.get_one('workflow_job_template_nodes', check_exists=(state == 'exists'), **{'data': search_fields}) if state == 'absent': # If the state was absent we can let the module delete it if needed, the module will handle exiting from this diff --git a/ansible_collections/awx/awx/plugins/modules/workflow_launch.py b/ansible_collections/awx/awx/plugins/modules/workflow_launch.py index 1613e4fa8..c06841098 100644 --- a/ansible_collections/awx/awx/plugins/modules/workflow_launch.py +++ b/ansible_collections/awx/awx/plugins/modules/workflow_launch.py @@ -27,13 +27,13 @@ options: - workflow_template organization: description: - - Organization the workflow job template exists in. + - Organization name, ID, or named URL the workflow job template exists in. - Used to help lookup the object, cannot be modified using this module. - If not provided, will lookup by name only, which does not work with duplicates. type: str inventory: description: - - Inventory to use for the job ran with this workflow, only used if prompt for inventory is set. + - Inventory name, ID, or named URL to use for the job ran with this workflow, only used if prompt for inventory is set. type: str limit: description: @@ -91,7 +91,6 @@ EXAMPLES = ''' ''' from ..module_utils.controller_api import ControllerAPIModule -import json def main(): @@ -116,15 +115,18 @@ def main(): name = module.params.get('name') organization = module.params.get('organization') inventory = module.params.get('inventory') - optional_args['limit'] = module.params.get('limit') wait = module.params.get('wait') interval = module.params.get('interval') timeout = module.params.get('timeout') - # Special treatment of extra_vars parameter - extra_vars = module.params.get('extra_vars') - if extra_vars is not None: - optional_args['extra_vars'] = json.dumps(extra_vars) + for field_name in ( + 'limit', + 'extra_vars', + 'scm_branch', + ): + field_val = module.params.get(field_name) + if field_val is not None: + optional_args[field_name] = field_val # Create a datastructure to pass into our job launch post_data = {} diff --git a/ansible_collections/awx/awx/test/awx/conftest.py b/ansible_collections/awx/awx/test/awx/conftest.py index 626f85936..5bf288ba6 100644 --- a/ansible_collections/awx/awx/test/awx/conftest.py +++ b/ansible_collections/awx/awx/test/awx/conftest.py @@ -18,28 +18,53 @@ import pytest from ansible.module_utils.six import raise_from from awx.main.tests.functional.conftest import _request -from awx.main.models import Organization, Project, Inventory, JobTemplate, Credential, CredentialType, ExecutionEnvironment, UnifiedJob +from awx.main.tests.functional.conftest import credentialtype_scm, credentialtype_ssh # noqa: F401; pylint: disable=unused-variable +from awx.main.models import ( + Organization, + Project, + Inventory, + JobTemplate, + Credential, + CredentialType, + ExecutionEnvironment, + UnifiedJob, + WorkflowJobTemplate, + NotificationTemplate, + Schedule, +) from django.db import transaction -try: - import tower_cli # noqa - HAS_TOWER_CLI = True -except ImportError: - HAS_TOWER_CLI = False +HAS_TOWER_CLI = False +HAS_AWX_KIT = False +logger = logging.getLogger('awx.main.tests') -try: - # Because awxkit will be a directory at the root of this makefile and we are using python3, import awxkit will work even if its not installed. - # However, awxkit will not contain api whih causes a stack failure down on line 170 when we try to mock it. - # So here we are importing awxkit.api to prevent that. Then you only get an error on tests for awxkit functionality. - import awxkit.api # noqa - HAS_AWX_KIT = True -except ImportError: - HAS_AWX_KIT = False +@pytest.fixture(autouse=True) +def awxkit_path_set(monkeypatch): + """Monkey patch sys.path, insert awxkit source code so that + the package does not need to be installed. + """ + base_folder = os.path.abspath(os.path.join(os.path.dirname(__file__), os.pardir, os.pardir, os.pardir, 'awxkit')) + monkeypatch.syspath_prepend(base_folder) -logger = logging.getLogger('awx.main.tests') + +@pytest.fixture(autouse=True) +def import_awxkit(): + global HAS_TOWER_CLI + global HAS_AWX_KIT + try: + import tower_cli # noqa + HAS_TOWER_CLI = True + except ImportError: + HAS_TOWER_CLI = False + + try: + import awxkit # noqa + HAS_AWX_KIT = True + except ImportError: + HAS_AWX_KIT = False def sanitize_dict(din): @@ -123,7 +148,7 @@ def run_module(request, collection_import): sanitize_dict(py_data) resp._content = bytes(json.dumps(django_response.data), encoding='utf8') resp.status_code = django_response.status_code - resp.headers = {'X-API-Product-Name': 'AWX', 'X-API-Product-Version': '0.0.1-devel'} + resp.headers = dict(django_response.headers) if request.config.getoption('verbose') > 0: logger.info('%s %s by %s, code:%s', method, '/api/' + url.split('/api/')[1], request_user.username, resp.status_code) @@ -156,10 +181,8 @@ def run_module(request, collection_import): resource_class = resource_module.ControllerAWXKitModule elif getattr(resource_module, 'ControllerAPIModule', None): resource_class = resource_module.ControllerAPIModule - elif getattr(resource_module, 'TowerLegacyModule', None): - resource_class = resource_module.TowerLegacyModule else: - raise RuntimeError("The module has neither a TowerLegacyModule, ControllerAWXKitModule or a ControllerAPIModule") + raise RuntimeError("The module has neither a ControllerAWXKitModule or a ControllerAPIModule") with mock.patch.object(resource_class, '_load_params', new=mock_load_params): # Call the test utility (like a mock server) instead of issuing HTTP requests @@ -236,10 +259,8 @@ def job_template(project, inventory): @pytest.fixture -def machine_credential(organization): - ssh_type = CredentialType.defaults['ssh']() - ssh_type.save() - return Credential.objects.create(credential_type=ssh_type, name='machine-cred', inputs={'username': 'test_user', 'password': 'pas4word'}) +def machine_credential(credentialtype_ssh, organization): # noqa: F811 + return Credential.objects.create(credential_type=credentialtype_ssh, name='machine-cred', inputs={'username': 'test_user', 'password': 'pas4word'}) @pytest.fixture @@ -253,9 +274,7 @@ def vault_credential(organization): def kube_credential(): ct = CredentialType.defaults['kubernetes_bearer_token']() ct.save() - return Credential.objects.create( - credential_type=ct, name='kube-cred', inputs={'host': 'my.cluster', 'bearer_token': 'my-token', 'verify_ssl': False} - ) + return Credential.objects.create(credential_type=ct, name='kube-cred', inputs={'host': 'my.cluster', 'bearer_token': 'my-token', 'verify_ssl': False}) @pytest.fixture @@ -288,3 +307,42 @@ def mock_has_unpartitioned_events(): # We mock this out to circumvent the migration query. with mock.patch.object(UnifiedJob, 'has_unpartitioned_events', new=False) as _fixture: yield _fixture + + +@pytest.fixture +def workflow_job_template(organization, inventory): + return WorkflowJobTemplate.objects.create(name='test-workflow_job_template', organization=organization, inventory=inventory) + + +@pytest.fixture +def notification_template(organization): + return NotificationTemplate.objects.create( + name='test-notification_template', + organization=organization, + notification_type="webhook", + notification_configuration=dict( + url="http://localhost", + username="", + password="", + headers={ + "Test": "Header", + }, + ), + ) + + +@pytest.fixture +def scm_credential(credentialtype_scm, organization): # noqa: F811 + return Credential.objects.create( + credential_type=credentialtype_scm, name='scm-cred', inputs={'username': 'optimus', 'password': 'prime'}, organization=organization + ) + + +@pytest.fixture +def rrule(): + return 'DTSTART:20151117T050000Z RRULE:FREQ=DAILY;INTERVAL=1;COUNT=1' + + +@pytest.fixture +def schedule(job_template, rrule): + return Schedule.objects.create(unified_job_template=job_template, name='test-sched', rrule=rrule) diff --git a/ansible_collections/awx/awx/test/awx/test_bulk.py b/ansible_collections/awx/awx/test/awx/test_bulk.py index 6ba97e900..da45621dd 100644 --- a/ansible_collections/awx/awx/test/awx/test_bulk.py +++ b/ansible_collections/awx/awx/test/awx/test_bulk.py @@ -10,7 +10,7 @@ from awx.main.models import WorkflowJob @pytest.mark.django_db def test_bulk_job_launch(run_module, admin_user, job_template): jobs = [dict(unified_job_template=job_template.id)] - run_module( + result = run_module( 'bulk_job_launch', { 'name': "foo-bulk-job", @@ -21,6 +21,8 @@ def test_bulk_job_launch(run_module, admin_user, job_template): }, admin_user, ) + assert not result.get('failed', False), result.get('msg', result) + assert result.get('changed'), result bulk_job = WorkflowJob.objects.get(name="foo-bulk-job") assert bulk_job.extra_vars == '{"animal": "owl"}' @@ -30,7 +32,7 @@ def test_bulk_job_launch(run_module, admin_user, job_template): @pytest.mark.django_db def test_bulk_host_create(run_module, admin_user, inventory): hosts = [dict(name="127.0.0.1"), dict(name="foo.dns.org")] - run_module( + result = run_module( 'bulk_host_create', { 'inventory': inventory.name, @@ -38,6 +40,33 @@ def test_bulk_host_create(run_module, admin_user, inventory): }, admin_user, ) + assert not result.get('failed', False), result.get('msg', result) + assert result.get('changed'), result resp_hosts = inventory.hosts.all().values_list('name', flat=True) for h in hosts: assert h['name'] in resp_hosts + + +@pytest.mark.django_db +def test_bulk_host_delete(run_module, admin_user, inventory): + hosts = [dict(name="127.0.0.1"), dict(name="foo.dns.org")] + result = run_module( + 'bulk_host_create', + { + 'inventory': inventory.name, + 'hosts': hosts, + }, + admin_user, + ) + assert not result.get('failed', False), result.get('msg', result) + assert result.get('changed'), result + resp_hosts_ids = list(inventory.hosts.all().values_list('id', flat=True)) + result = run_module( + 'bulk_host_delete', + { + 'hosts': resp_hosts_ids, + }, + admin_user, + ) + assert not result.get('failed', False), result.get('msg', result) + assert result.get('changed'), result diff --git a/ansible_collections/awx/awx/test/awx/test_completeness.py b/ansible_collections/awx/awx/test/awx/test_completeness.py index ef3d70727..42d013df6 100644 --- a/ansible_collections/awx/awx/test/awx/test_completeness.py +++ b/ansible_collections/awx/awx/test/awx/test_completeness.py @@ -20,7 +20,9 @@ read_only_endpoints_with_modules = ['settings', 'role', 'project_update', 'workf # If a module should not be created for an endpoint and the endpoint is not read-only add it here # THINK HARD ABOUT DOING THIS -no_module_for_endpoint = [] +no_module_for_endpoint = [ + 'constructed_inventory', # This is a view for inventory with kind=constructed +] # Some modules work on the related fields of an endpoint. These modules will not have an auto-associated endpoint no_endpoint_for_module = [ @@ -48,10 +50,11 @@ no_endpoint_for_module = [ extra_endpoints = { 'bulk_job_launch': '/api/v2/bulk/job_launch/', 'bulk_host_create': '/api/v2/bulk/host_create/', + 'bulk_host_delete': '/api/v2/bulk/host_delete/', } # Global module parameters we can ignore -ignore_parameters = ['state', 'new_name', 'update_secrets', 'copy_from'] +ignore_parameters = ['state', 'new_name', 'update_secrets', 'copy_from', 'is_internal'] # Some modules take additional parameters that do not appear in the API # Add the module name as the key with the value being the list of params to ignore @@ -245,7 +248,7 @@ def test_completeness(collection_import, request, admin_user, job_template, exec singular_endpoint = '{0}'.format(endpoint) if singular_endpoint.endswith('ies'): singular_endpoint = singular_endpoint[:-3] - if singular_endpoint != 'settings' and singular_endpoint.endswith('s'): + elif singular_endpoint != 'settings' and singular_endpoint.endswith('s'): singular_endpoint = singular_endpoint[:-1] module_name = '{0}'.format(singular_endpoint) diff --git a/ansible_collections/awx/awx/test/awx/test_credential.py b/ansible_collections/awx/awx/test/awx/test_credential.py index f12594bee..c67953197 100644 --- a/ansible_collections/awx/awx/test/awx/test_credential.py +++ b/ansible_collections/awx/awx/test/awx/test_credential.py @@ -132,3 +132,20 @@ def test_secret_field_write_twice(run_module, organization, admin_user, cred_typ else: assert result.get('changed') is False, result assert Credential.objects.get(id=result['id']).get_input('token') == val1 + + +@pytest.mark.django_db +@pytest.mark.parametrize('state', ('present', 'absent', 'exists')) +def test_credential_state(run_module, organization, admin_user, cred_type, state): + result = run_module( + 'credential', + dict( + name='Galaxy Token for Steve', + organization=organization.name, + credential_type=cred_type.name, + inputs={'token': '7rEZK38DJl58A7RxA6EC7lLvUHbBQ1'}, + state=state, + ), + admin_user, + ) + assert not result.get('failed', False), result.get('msg', result) diff --git a/ansible_collections/awx/awx/test/awx/test_export.py b/ansible_collections/awx/awx/test/awx/test_export.py new file mode 100644 index 000000000..70c8466ec --- /dev/null +++ b/ansible_collections/awx/awx/test/awx/test_export.py @@ -0,0 +1,150 @@ +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + +import pytest + +from awx.main.models.execution_environments import ExecutionEnvironment +from awx.main.models.jobs import JobTemplate + +from awx.main.tests.functional.conftest import user, system_auditor # noqa: F401; pylint: disable=unused-import + + +ASSETS = set([ + "users", + "organizations", + "teams", + "credential_types", + "credentials", + "notification_templates", + "projects", + "inventory", + "inventory_sources", + "job_templates", + "workflow_job_templates", + "execution_environments", + "applications", + "schedules", +]) + + +@pytest.fixture +def job_template(project, inventory, organization, machine_credential): + jt = JobTemplate.objects.create(name='test-jt', project=project, inventory=inventory, organization=organization, playbook='helloworld.yml') + jt.credentials.add(machine_credential) + jt.save() + return jt + + +@pytest.fixture +def execution_environment(organization): + return ExecutionEnvironment.objects.create(name="test-ee", description="test-ee", managed=False, organization=organization) + + +def find_by(result, name, key, value): + for c in result[name]: + if c[key] == value: + return c + values = [c.get(key, None) for c in result[name]] + raise ValueError(f"Failed to find assets['{name}'][{key}] = '{value}' valid values are {values}") + + +@pytest.mark.django_db +def test_export(run_module, admin_user): + """ + There should be nothing to export EXCEPT the admin user. + """ + result = run_module('export', dict(all=True), admin_user) + assert not result.get('failed', False), result.get('msg', result) + assets = result['assets'] + + assert set(result['assets'].keys()) == ASSETS + + u = find_by(assets, 'users', 'username', 'admin') + assert u['is_superuser'] is True + + all_assets_except_users = {k: v for k, v in assets.items() if k != 'users'} + + for k, v in all_assets_except_users.items(): + assert v == [], f"Expected resource {k} to be empty. Instead it is {v}" + + +@pytest.mark.django_db +def test_export_simple( + run_module, + organization, + project, + inventory, + job_template, + scm_credential, + machine_credential, + workflow_job_template, + execution_environment, + notification_template, + rrule, + schedule, + admin_user, +): + """ + TODO: Ensure there aren't _more_ results in each resource than we expect + """ + result = run_module('export', dict(all=True), admin_user) + assert not result.get('failed', False), result.get('msg', result) + assets = result['assets'] + + u = find_by(assets, 'users', 'username', 'admin') + assert u['is_superuser'] is True + + find_by(assets, 'organizations', 'name', 'Default') + + r = find_by(assets, 'credentials', 'name', 'scm-cred') + assert r['credential_type']['kind'] == 'scm' + assert r['credential_type']['name'] == 'Source Control' + + r = find_by(assets, 'credentials', 'name', 'machine-cred') + assert r['credential_type']['kind'] == 'ssh' + assert r['credential_type']['name'] == 'Machine' + + r = find_by(assets, 'job_templates', 'name', 'test-jt') + assert r['natural_key']['organization']['name'] == 'Default' + assert r['inventory']['name'] == 'test-inv' + assert r['project']['name'] == 'test-proj' + + find_by(r['related'], 'credentials', 'name', 'machine-cred') + + r = find_by(assets, 'inventory', 'name', 'test-inv') + assert r['organization']['name'] == 'Default' + + r = find_by(assets, 'projects', 'name', 'test-proj') + assert r['organization']['name'] == 'Default' + + r = find_by(assets, 'workflow_job_templates', 'name', 'test-workflow_job_template') + assert r['natural_key']['organization']['name'] == 'Default' + assert r['inventory']['name'] == 'test-inv' + + r = find_by(assets, 'execution_environments', 'name', 'test-ee') + assert r['organization']['name'] == 'Default' + + r = find_by(assets, 'schedules', 'name', 'test-sched') + assert r['rrule'] == rrule + + r = find_by(assets, 'notification_templates', 'name', 'test-notification_template') + assert r['organization']['name'] == 'Default' + assert r['notification_configuration']['url'] == 'http://localhost' + + +@pytest.mark.django_db +def test_export_system_auditor(run_module, organization, system_auditor): # noqa: F811 + """ + This test illustrates that export of resources can now happen + when ran as non-root user (i.e. system auditor). The OPTIONS + endpoint does NOT return POST for a system auditor, but now we + make a best-effort to parse the description string, which will + often have the fields. + """ + result = run_module('export', dict(all=True), system_auditor) + assert not result.get('failed', False), result.get('msg', result) + assert 'msg' not in result + assert 'assets' in result + + find_by(result['assets'], 'organizations', 'name', 'Default') diff --git a/ansible_collections/awx/awx/test/awx/test_instance.py b/ansible_collections/awx/awx/test/awx/test_instance.py new file mode 100644 index 000000000..06d1fe94b --- /dev/null +++ b/ansible_collections/awx/awx/test/awx/test_instance.py @@ -0,0 +1,46 @@ +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + +import pytest + +from awx.main.models import Instance +from django.test.utils import override_settings + + +@pytest.mark.django_db +def test_peers_adding_and_removing(run_module, admin_user): + with override_settings(IS_K8S=True): + result = run_module( + 'instance', + {'hostname': 'hopnode', 'node_type': 'hop', 'node_state': 'installed', 'listener_port': 6789}, + admin_user, + ) + assert result['changed'] + + hop_node = Instance.objects.get(pk=result.get('id')) + + assert hop_node.node_type == 'hop' + + address = hop_node.receptor_addresses.get(pk=result.get('id')) + assert address.port == 6789 + + result = run_module( + 'instance', + {'hostname': 'executionnode', 'node_type': 'execution', 'node_state': 'installed', 'peers': ['hopnode']}, + admin_user, + ) + assert result['changed'] + + execution_node = Instance.objects.get(pk=result.get('id')) + + assert set(execution_node.peers.all()) == {address} + + result = run_module( + 'instance', + {'hostname': 'executionnode', 'node_type': 'execution', 'node_state': 'installed', 'peers': []}, + admin_user, + ) + + assert result['changed'] + assert set(execution_node.peers.all()) == set() diff --git a/ansible_collections/awx/awx/test/awx/test_job_template.py b/ansible_collections/awx/awx/test/awx/test_job_template.py index e785a63a3..5ee114359 100644 --- a/ansible_collections/awx/awx/test/awx/test_job_template.py +++ b/ansible_collections/awx/awx/test/awx/test_job_template.py @@ -2,9 +2,11 @@ from __future__ import absolute_import, division, print_function __metaclass__ = type +import random + import pytest -from awx.main.models import ActivityStream, JobTemplate, Job, NotificationTemplate +from awx.main.models import ActivityStream, JobTemplate, Job, NotificationTemplate, Label @pytest.mark.django_db @@ -244,6 +246,42 @@ def test_job_template_with_survey_encrypted_default(run_module, admin_user, proj @pytest.mark.django_db +def test_associate_changed_status(run_module, admin_user, organization, project): + # create JT and labels + jt = JobTemplate.objects.create(name='foo', project=project, playbook='helloworld.yml') + labels = [Label.objects.create(name=f'foo{i}', organization=organization) for i in range(10)] + + # sanity: no-op without labels involved + result = run_module('job_template', dict(name=jt.name, playbook='helloworld.yml'), admin_user) + assert not result.get('failed', False), result.get('msg', result) + assert result['changed'] is False + + # first time adding labels, this should make the label list equal to what was specified + result = run_module('job_template', dict(name=jt.name, playbook='helloworld.yml', labels=[l.name for l in labels]), admin_user) + assert not result.get('failed', False), result.get('msg', result) + assert result['changed'] + assert set(l.id for l in jt.labels.all()) == set(l.id for l in labels) + + # shuffling the labels should not result in any change + random.shuffle(labels) + result = run_module('job_template', dict(name=jt.name, playbook='helloworld.yml', labels=[l.name for l in labels]), admin_user) + assert not result.get('failed', False), result.get('msg', result) + assert result['changed'] is False + + # not specifying labels should not change labels + result = run_module('job_template', dict(name=jt.name, playbook='helloworld.yml'), admin_user) + assert not result.get('failed', False), result.get('msg', result) + assert result['changed'] is False + + # should be able to remove only some labels + fewer_labels = labels[:7] + result = run_module('job_template', dict(name=jt.name, playbook='helloworld.yml', labels=[l.name for l in fewer_labels]), admin_user) + assert not result.get('failed', False), result.get('msg', result) + assert result['changed'] + assert set(l.id for l in jt.labels.all()) == set(l.id for l in fewer_labels) + + +@pytest.mark.django_db def test_associate_only_on_success(run_module, admin_user, organization, project): jt = JobTemplate.objects.create( name='foo', diff --git a/ansible_collections/awx/awx/test/awx/test_module_utils.py b/ansible_collections/awx/awx/test/awx/test_module_utils.py index 088b5368a..cbdc172a3 100644 --- a/ansible_collections/awx/awx/test/awx/test_module_utils.py +++ b/ansible_collections/awx/awx/test/awx/test_module_utils.py @@ -76,21 +76,6 @@ def test_version_warning(collection_import, silence_warning): ) -def test_no_version_warning(collection_import, silence_warning): - ControllerAPIModule = collection_import('plugins.module_utils.controller_api').ControllerAPIModule - cli_data = {'ANSIBLE_MODULE_ARGS': {}} - testargs = ['module_file2.py', json.dumps(cli_data)] - with mock.patch.object(sys, 'argv', testargs): - with mock.patch('ansible.module_utils.urls.Request.open', new=mock_no_ping_response): - my_module = ControllerAPIModule(argument_spec=dict()) - my_module._COLLECTION_VERSION = "2.0.0" - my_module._COLLECTION_TYPE = "awx" - my_module.get_endpoint('ping') - silence_warning.assert_called_once_with( - 'You are using the {0} version of this collection but connecting to a controller that did not return a version'.format(my_module._COLLECTION_VERSION) - ) - - def test_version_warning_strictness_awx(collection_import, silence_warning): ControllerAPIModule = collection_import('plugins.module_utils.controller_api').ControllerAPIModule cli_data = {'ANSIBLE_MODULE_ARGS': {}} diff --git a/ansible_collections/awx/awx/test/awx/test_notification_template.py b/ansible_collections/awx/awx/test/awx/test_notification_template.py index 8bd2647b0..346ac4105 100644 --- a/ansible_collections/awx/awx/test/awx/test_notification_template.py +++ b/ansible_collections/awx/awx/test/awx/test_notification_template.py @@ -155,4 +155,4 @@ def test_build_notification_message_undefined(run_module, admin_user, organizati nt = NotificationTemplate.objects.get(id=result['id']) body = job.build_notification_message(nt, 'running') - assert '{"started_by": "My Placeholder"}' in body[1] + assert 'The template rendering return a blank body' in body[1] diff --git a/ansible_collections/awx/awx/test/awx/test_organization.py b/ansible_collections/awx/awx/test/awx/test_organization.py index 08d35ed6f..e6b3cc5e2 100644 --- a/ansible_collections/awx/awx/test/awx/test_organization.py +++ b/ansible_collections/awx/awx/test/awx/test_organization.py @@ -3,8 +3,9 @@ from __future__ import absolute_import, division, print_function __metaclass__ = type import pytest +import random -from awx.main.models import Organization +from awx.main.models import Organization, Credential, CredentialType @pytest.mark.django_db @@ -30,3 +31,63 @@ def test_create_organization(run_module, admin_user): assert result == {"name": "foo", "changed": True, "id": org.id, "invocation": {"module_args": module_args}} assert org.description == 'barfoo' + + +@pytest.mark.django_db +def test_galaxy_credential_order(run_module, admin_user): + org = Organization.objects.create(name='foo') + cred_type = CredentialType.defaults['galaxy_api_token']() + cred_type.save() + + cred_ids = [] + for number in range(1, 10): + new_cred = Credential.objects.create(name=f"Galaxy Credential {number}", credential_type=cred_type, organization=org, inputs={'url': 'www.redhat.com'}) + cred_ids.append(new_cred.id) + + random.shuffle(cred_ids) + + module_args = { + 'name': 'foo', + 'state': 'present', + 'controller_host': None, + 'controller_username': None, + 'controller_password': None, + 'validate_certs': None, + 'controller_oauthtoken': None, + 'controller_config_file': None, + 'galaxy_credentials': cred_ids, + } + + result = run_module('organization', module_args, admin_user) + print(result) + assert result['changed'] is True + + cred_order_in_org = [] + for a_cred in org.galaxy_credentials.all(): + cred_order_in_org.append(a_cred.id) + + assert cred_order_in_org == cred_ids + + # Shuffle them up and try again to make sure a new order is honored + random.shuffle(cred_ids) + + module_args = { + 'name': 'foo', + 'state': 'present', + 'controller_host': None, + 'controller_username': None, + 'controller_password': None, + 'validate_certs': None, + 'controller_oauthtoken': None, + 'controller_config_file': None, + 'galaxy_credentials': cred_ids, + } + + result = run_module('organization', module_args, admin_user) + assert result['changed'] is True + + cred_order_in_org = [] + for a_cred in org.galaxy_credentials.all(): + cred_order_in_org.append(a_cred.id) + + assert cred_order_in_org == cred_ids diff --git a/ansible_collections/awx/awx/tests/integration/targets/ad_hoc_command/tasks/main.yml b/ansible_collections/awx/awx/tests/integration/targets/ad_hoc_command/tasks/main.yml index 316315df3..3180045e8 100644 --- a/ansible_collections/awx/awx/tests/integration/targets/ad_hoc_command/tasks/main.yml +++ b/ansible_collections/awx/awx/tests/integration/targets/ad_hoc_command/tasks/main.yml @@ -9,6 +9,7 @@ inv_name: "AWX-Collection-tests-ad_hoc_command-inventory-{{ test_id }}" ssh_cred_name: "AWX-Collection-tests-ad_hoc_command-ssh-cred-{{ test_id }}" org_name: "AWX-Collection-tests-ad_hoc_command-org-{{ test_id }}" + ee_name: "AWX-Collection-tests-ad_hoc_command-ee-{{ test_id }}" - name: Create a New Organization organization: @@ -34,6 +35,16 @@ credential_type: 'Machine' state: present +- name: Create an Execution Environment + execution_environment: + name: "{{ ee_name }}" + organization: "{{ org_name }}" + description: "EE for Testing" + image: quay.io/ansible/awx-ee + pull: always + state: present + register: result_ee + - name: Launch an Ad Hoc Command waiting for it to finish ad_hoc_command: inventory: "{{ inv_name }}" @@ -61,6 +72,36 @@ - "result is changed" - "result.status == 'successful'" +- name: Launch an Ad Hoc Command with extra_vars + ad_hoc_command: + inventory: "Demo Inventory" + credential: "{{ ssh_cred_name }}" + module_name: "ping" + extra_vars: + var1: "test var" + wait: true + register: result + +- assert: + that: + - "result is changed" + - "result.status == 'successful'" + +- name: Launch an Ad Hoc Command with Execution Environment specified + ad_hoc_command: + inventory: "Demo Inventory" + credential: "{{ ssh_cred_name }}" + execution_environment: "{{ ee_name }}" + module_name: "ping" + wait: true + register: result + +- assert: + that: + - "result is changed" + - "result.status == 'successful'" + - "lookup('awx.awx.controller_api', 'ad_hoc_commands/' ~ result.id)['execution_environment'] == result_ee.id" + - name: Check module fails with correct msg ad_hoc_command: inventory: "{{ inv_name }}" @@ -75,6 +116,13 @@ - "result is not changed" - "'Does not exist' in result.response['json']['module_name'][0]" +- name: Delete the Execution Environment + execution_environment: + name: "{{ ee_name }}" + organization: "{{ org_name }}" + image: quay.io/ansible/awx-ee + state: absent + - name: Delete the Credential credential: name: "{{ ssh_cred_name }}" diff --git a/ansible_collections/awx/awx/tests/integration/targets/ad_hoc_command_cancel/tasks/main.yml b/ansible_collections/awx/awx/tests/integration/targets/ad_hoc_command_cancel/tasks/main.yml index f7ffe9bc9..55c1803db 100644 --- a/ansible_collections/awx/awx/tests/integration/targets/ad_hoc_command_cancel/tasks/main.yml +++ b/ansible_collections/awx/awx/tests/integration/targets/ad_hoc_command_cancel/tasks/main.yml @@ -46,27 +46,24 @@ that: - "command is changed" -- name: Timeout waiting for the command to cancel +- name: Cancel the command ad_hoc_command_cancel: command_id: "{{ command.id }}" - timeout: -1 + request_timeout: 60 register: results - ignore_errors: true - assert: that: - - results is failed - - "results['msg'] == 'Monitoring of ad hoc command aborted due to timeout'" - -- block: - - name: "Wait for up to a minute until the job enters the can_cancel: False state" - debug: - msg: "The job can_cancel status has transitioned into False, we can proceed with testing" - until: not job_status - retries: 6 - delay: 10 - vars: - job_status: "{{ lookup('awx.awx.controller_api', 'ad_hoc_commands/'+ command.id | string +'/cancel')['can_cancel'] }}" + - results is changed + +- name: "Wait for up to a minute until the job enters the can_cancel: False state" + debug: + msg: "The job can_cancel status has transitioned into False, we can proceed with testing" + until: not job_status + retries: 6 + delay: 10 + vars: + job_status: "{{ lookup('awx.awx.controller_api', 'ad_hoc_commands/'+ command.id | string +'/cancel')['can_cancel'] }}" - name: Cancel the command with hard error if it's not running ad_hoc_command_cancel: diff --git a/ansible_collections/awx/awx/tests/integration/targets/ad_hoc_command_wait/tasks/main.yml b/ansible_collections/awx/awx/tests/integration/targets/ad_hoc_command_wait/tasks/main.yml index 941774874..61bfd42fc 100644 --- a/ansible_collections/awx/awx/tests/integration/targets/ad_hoc_command_wait/tasks/main.yml +++ b/ansible_collections/awx/awx/tests/integration/targets/ad_hoc_command_wait/tasks/main.yml @@ -108,9 +108,9 @@ - assert: that: - - wait_results is failed - - 'wait_results.status == "canceled"' - - "wait_results.msg == 'The ad hoc command - {{ command.id }}, failed'" + - 'wait_results.status in ["successful", "canceled"]' + fail_msg: "Ad hoc command stdout: {{ lookup('awx.awx.controller_api', 'ad_hoc_commands/' + command.id | string + '/stdout/?format=json') }}" + success_msg: "Ad hoc command finished with status {{ wait_results.status }}" - name: Delete the Credential credential: diff --git a/ansible_collections/awx/awx/tests/integration/targets/application/tasks/main.yml b/ansible_collections/awx/awx/tests/integration/targets/application/tasks/main.yml index ba76763a4..864d4d0ac 100644 --- a/ansible_collections/awx/awx/tests/integration/targets/application/tasks/main.yml +++ b/ansible_collections/awx/awx/tests/integration/targets/application/tasks/main.yml @@ -2,6 +2,7 @@ - name: Generate a test id set_fact: test_id: "{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}" + when: test_id is not defined - name: Generate names set_fact: @@ -23,6 +24,43 @@ that: - "result is changed" + - name: Run an application with exists + application: + name: "{{ app1_name }}" + authorization_grant_type: "password" + client_type: "public" + organization: "Default" + state: exists + register: result + + - assert: + that: + - "result is not changed" + + - name: Delete our application + application: + name: "{{ app1_name }}" + organization: "Default" + state: absent + register: result + + - assert: + that: + - "result is changed" + + - name: Run an application with exists + application: + name: "{{ app1_name }}" + authorization_grant_type: "password" + client_type: "public" + organization: "Default" + state: exists + register: result + + - assert: + that: + - "result is changed" + - name: Delete our application application: name: "{{ app1_name }}" diff --git a/ansible_collections/awx/awx/tests/integration/targets/bulk_host_create/main.yml b/ansible_collections/awx/awx/tests/integration/targets/bulk_host_create/tasks/main.yml index 892154265..892154265 100644 --- a/ansible_collections/awx/awx/tests/integration/targets/bulk_host_create/main.yml +++ b/ansible_collections/awx/awx/tests/integration/targets/bulk_host_create/tasks/main.yml diff --git a/ansible_collections/awx/awx/tests/integration/targets/bulk_host_delete/tasks/main.yml b/ansible_collections/awx/awx/tests/integration/targets/bulk_host_delete/tasks/main.yml new file mode 100644 index 000000000..5f38efe7c --- /dev/null +++ b/ansible_collections/awx/awx/tests/integration/targets/bulk_host_delete/tasks/main.yml @@ -0,0 +1,80 @@ +--- +- name: "Generate a random string for test" + set_fact: + test_id: "{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}" + when: "test_id is not defined" + +- name: "Generate a unique name" + set_fact: + bulk_inv_name: "AWX-Collection-tests-bulk_host_create-{{ test_id }}" + +- name: "Get our collection package" + controller_meta: + register: "controller_meta" + +- name: "Generate the name of our plugin" + set_fact: + plugin_name: "{{ controller_meta.prefix }}.controller_api" + +- name: "Create an inventory" + inventory: + name: "{{ bulk_inv_name }}" + organization: "Default" + state: "present" + register: "inventory_result" + +- name: "Bulk Host Create" + bulk_host_create: + hosts: + - name: "123.456.789.123" + description: "myhost1" + variables: + food: "carrot" + color: "orange" + - name: "example.dns.gg" + description: "myhost2" + enabled: "false" + inventory: "{{ bulk_inv_name }}" + register: "result" + +- assert: + that: + - "result is not failed" + +- name: "Get our collection package" + controller_meta: + register: "controller_meta" + +- name: "Generate the name of our plugin" + set_fact: + plugin_name: "{{ controller_meta.prefix }}.controller_api" + +- name: "Setting the inventory hosts endpoint" + set_fact: + endpoint: "inventories/{{ inventory_result.id }}/hosts/" + +- name: "Get hosts information from inventory" + set_fact: + hosts_created: "{{ query(plugin_name, endpoint, return_objects=True) }}" + host_id_list: [] + +- name: "Extract host IDs from hosts information" + set_fact: + host_id_list: "{{ host_id_list + [item.id] }}" + loop: "{{ hosts_created }}" + +- name: "Bulk Host Delete" + bulk_host_delete: + hosts: "{{ host_id_list }}" + register: "result" + +- assert: + that: + - "result is not failed" + +# cleanup +- name: "Delete inventory" + inventory: + name: "{{ bulk_inv_name }}" + organization: "Default" + state: "absent" diff --git a/ansible_collections/awx/awx/tests/integration/targets/bulk_job_launch/main.yml b/ansible_collections/awx/awx/tests/integration/targets/bulk_job_launch/tasks/main.yml index cd152a8ee..f4a107ecf 100644 --- a/ansible_collections/awx/awx/tests/integration/targets/bulk_job_launch/main.yml +++ b/ansible_collections/awx/awx/tests/integration/targets/bulk_job_launch/tasks/main.yml @@ -60,7 +60,7 @@ - result['job_info']['skip_tags'] == "skipbaz" - result['job_info']['limit'] == "localhost" - result['job_info']['job_tags'] == "Hello World" - - result['job_info']['inventory'] == {{ inventory_id }} + - result['job_info']['inventory'] == inventory_id | int - "result['job_info']['extra_vars'] == '{\"animal\": \"bear\", \"food\": \"carrot\"}'" # cleanup diff --git a/ansible_collections/awx/awx/tests/integration/targets/credential/tasks/main.yml b/ansible_collections/awx/awx/tests/integration/targets/credential/tasks/main.yml index 57b2168f2..34dd058d9 100644 --- a/ansible_collections/awx/awx/tests/integration/targets/credential/tasks/main.yml +++ b/ansible_collections/awx/awx/tests/integration/targets/credential/tasks/main.yml @@ -47,6 +47,55 @@ that: - "result is changed" +- name: Create an Org-specific credential with an ID with exists + credential: + name: "{{ ssh_cred_name1 }}" + organization: Default + credential_type: Machine + state: exists + register: result + +- assert: + that: + - "result is not changed" + +- name: Delete an Org-specific credential with an ID + credential: + name: "{{ ssh_cred_name1 }}" + organization: Default + credential_type: Machine + state: absent + register: result + +- assert: + that: + - "result is changed" + +- name: Delete a credential without credential_type + credential: + name: "{{ ssh_cred_name1 }}" + organization: Default + state: absent + register: result + ignore_errors: true + +- assert: + that: + - "result is failed" + + +- name: Create an Org-specific credential with an ID with exists + credential: + name: "{{ ssh_cred_name1 }}" + organization: Default + credential_type: Machine + state: exists + register: result + +- assert: + that: + - "result is changed" + - name: Delete a Org-specific credential credential: name: "{{ ssh_cred_name1 }}" @@ -178,6 +227,60 @@ that: - result is changed +- name: Delete an SSH credential + credential: + name: "{{ ssh_cred_name2 }}" + organization: Default + state: absent + credential_type: Machine + register: result + +- assert: + that: + - "result is changed" + +- name: Ensure existence of SSH credential + credential: + name: "{{ ssh_cred_name2 }}" + organization: Default + state: exists + credential_type: Machine + description: An example SSH awx.awx.credential + inputs: + username: joe + password: secret + become_method: sudo + become_username: superuser + become_password: supersecret + ssh_key_data: "{{ ssh_key_data }}" + ssh_key_unlock: "passphrase" + register: result + +- assert: + that: + - result is changed + +- name: Ensure existence of SSH credential, not updating any inputs + credential: + name: "{{ ssh_cred_name2 }}" + organization: Default + state: exists + credential_type: Machine + description: An example SSH awx.awx.credential + inputs: + username: joe + password: no-update-secret + become_method: sudo + become_username: some-other-superuser + become_password: some-other-secret + ssh_key_data: "{{ ssh_key_data }}" + ssh_key_unlock: "another-pass-phrase" + register: result + +- assert: + that: + - result is not changed + - name: Create an invalid SSH credential (passphrase required) credential: name: SSH Credential diff --git a/ansible_collections/awx/awx/tests/integration/targets/credential_input_source/tasks/main.yml b/ansible_collections/awx/awx/tests/integration/targets/credential_input_source/tasks/main.yml index e9066a0f2..1d56d5e4b 100644 --- a/ansible_collections/awx/awx/tests/integration/targets/credential_input_source/tasks/main.yml +++ b/ansible_collections/awx/awx/tests/integration/targets/credential_input_source/tasks/main.yml @@ -54,6 +54,51 @@ that: - "result is changed" + - name: Add credential Input Source with exists + credential_input_source: + input_field_name: password + target_credential: "{{ target_cred_result.id }}" + source_credential: "{{ src_cred_result.id }}" + metadata: + object_query: "Safe=MY_SAFE;Object=AWX-user" + object_query_format: "Exact" + state: exists + register: result + + - assert: + that: + - "result is not changed" + + - name: Delete credential Input Source + credential_input_source: + input_field_name: password + target_credential: "{{ target_cred_result.id }}" + source_credential: "{{ src_cred_result.id }}" + metadata: + object_query: "Safe=MY_SAFE;Object=AWX-user" + object_query_format: "Exact" + state: absent + register: result + + - assert: + that: + - "result is changed" + + - name: Add credential Input Source with exists + credential_input_source: + input_field_name: password + target_credential: "{{ target_cred_result.id }}" + source_credential: "{{ src_cred_result.id }}" + metadata: + object_query: "Safe=MY_SAFE;Object=AWX-user" + object_query_format: "Exact" + state: exists + register: result + + - assert: + that: + - "result is changed" + - name: Add Second credential Lookup credential: description: Credential for Testing Source Change diff --git a/ansible_collections/awx/awx/tests/integration/targets/credential_type/tasks/main.yml b/ansible_collections/awx/awx/tests/integration/targets/credential_type/tasks/main.yml index e3b75f765..11bd6f904 100644 --- a/ansible_collections/awx/awx/tests/integration/targets/credential_type/tasks/main.yml +++ b/ansible_collections/awx/awx/tests/integration/targets/credential_type/tasks/main.yml @@ -1,7 +1,12 @@ --- +- name: Generate a test ID + set_fact: + test_id: "{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}" + when: test_id is not defined + - name: Generate names set_fact: - cred_type_name: "AWX-Collection-tests-credential_type-cred-type-{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}" + cred_type_name: "AWX-Collection-tests-credential_type-cred-type-{{ test_id }}" - block: - name: Add Tower credential type @@ -17,6 +22,48 @@ that: - "result is changed" + - name: Add Tower credential type with exists + credential_type: + description: Credential type for Test + name: "{{ cred_type_name }}" + kind: cloud + inputs: {"fields": [{"type": "string", "id": "username", "label": "Username"}, {"secret": true, "type": "string", "id": "password", "label": "Password"}], "required": ["username", "password"]} + injectors: {"extra_vars": {"test": "foo"}} + state: exists + register: result + + - assert: + that: + - "result is not changed" + + - name: Delete the credential type + credential_type: + description: Credential type for Test + name: "{{ cred_type_name }}" + kind: cloud + inputs: {"fields": [{"type": "string", "id": "username", "label": "Username"}, {"secret": true, "type": "string", "id": "password", "label": "Password"}], "required": ["username", "password"]} + injectors: {"extra_vars": {"test": "foo"}} + state: absent + register: result + + - assert: + that: + - "result is changed" + + - name: Add Tower credential type with exists + credential_type: + description: Credential type for Test + name: "{{ cred_type_name }}" + kind: cloud + inputs: {"fields": [{"type": "string", "id": "username", "label": "Username"}, {"secret": true, "type": "string", "id": "password", "label": "Password"}], "required": ["username", "password"]} + injectors: {"extra_vars": {"test": "foo"}} + state: exists + register: result + + - assert: + that: + - "result is changed" + - name: Rename Tower credential type credential_type: name: "{{ cred_type_name }}" diff --git a/ansible_collections/awx/awx/tests/integration/targets/demo_data/tasks/main.yml b/ansible_collections/awx/awx/tests/integration/targets/demo_data/tasks/main.yml index db152671b..3052a0ebf 100644 --- a/ansible_collections/awx/awx/tests/integration/targets/demo_data/tasks/main.yml +++ b/ansible_collections/awx/awx/tests/integration/targets/demo_data/tasks/main.yml @@ -33,6 +33,7 @@ name: "localhost" inventory: "Demo Inventory" state: present + enabled: true variables: ansible_connection: local register: result diff --git a/ansible_collections/awx/awx/tests/integration/targets/execution_environment/tasks/main.yml b/ansible_collections/awx/awx/tests/integration/targets/execution_environment/tasks/main.yml index 0cb2fd90b..f1758bd1b 100644 --- a/ansible_collections/awx/awx/tests/integration/targets/execution_environment/tasks/main.yml +++ b/ansible_collections/awx/awx/tests/integration/targets/execution_environment/tasks/main.yml @@ -22,6 +22,48 @@ that: - "result is changed" + - name: Add an EE with exists + execution_environment: + name: "{{ ee_name }}" + description: "EE for Testing" + image: quay.io/ansible/awx-ee + pull: always + organization: Default + state: exists + register: result + + - assert: + that: + - "result is not changed" + + - name: Delete an EE + execution_environment: + name: "{{ ee_name }}" + description: "EE for Testing" + image: quay.io/ansible/awx-ee + pull: always + organization: Default + state: absent + register: result + + - assert: + that: + - "result is changed" + + - name: Add an EE with exists + execution_environment: + name: "{{ ee_name }}" + description: "EE for Testing" + image: quay.io/ansible/awx-ee + pull: always + organization: Default + state: exists + register: result + + - assert: + that: + - "result is changed" + - name: Associate the Test EE with Default Org (this should fail) execution_environment: name: "{{ ee_name }}" diff --git a/ansible_collections/awx/awx/tests/integration/targets/group/tasks/main.yml b/ansible_collections/awx/awx/tests/integration/targets/group/tasks/main.yml index ac58826b1..c9a492911 100644 --- a/ansible_collections/awx/awx/tests/integration/targets/group/tasks/main.yml +++ b/ansible_collections/awx/awx/tests/integration/targets/group/tasks/main.yml @@ -1,25 +1,54 @@ --- +- name: Generate a test ID + set_fact: + test_id: "{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}" + when: test_id is not defined + - name: Generate names set_fact: - group_name1: "AWX-Collection-tests-group-group-{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}" - group_name2: "AWX-Collection-tests-group-group-{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}" - group_name3: "AWX-Collection-tests-group-group-{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}" - inv_name: "AWX-Collection-tests-group-inv-{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}" - host_name1: "AWX-Collection-tests-group-host-{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}" - host_name2: "AWX-Collection-tests-group-host-{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}" - host_name3: "AWX-Collection-tests-group-host-{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}" + group_name1: "AWX-Collection-tests-group-group1-{{ test_id }}" + group_name2: "AWX-Collection-tests-group-group2-{{ test_id }}" + group_name3: "AWX-Collection-tests-group-group3-{{ test_id }}" + group_name4: "AWX-Collection-tests-group-group4-{{ test_id }}" + inv_name: "AWX-Collection-tests-group-inv-{{ test_id }}" + host_name1: "AWX-Collection-tests-group-host1-{{ test_id }}" + host_name2: "AWX-Collection-tests-group-host2-{{ test_id }}" + host_name3: "AWX-Collection-tests-group-host3-{{ test_id }}" + host_name4: "AWX-Collection-tests-group-host4-{{ test_id }}" - name: Create an Inventory inventory: name: "{{ inv_name }}" organization: Default state: present - register: result + register: inv_result + +- name: Create a Host + host: + name: "{{ host_name4 }}" + inventory: "{{ inv_name }}" + state: present + register: host_result + +- name: Add Host to Group + group: + name: "{{ group_name1 }}" + inventory: "{{ inv_name }}" + hosts: + - "{{ host_name4 }}" + preserve_existing_hosts: true + register: group_result + +- assert: + that: + - inv_result is changed + - host_result is changed + - group_result is changed -- name: Create a Group +- name: Create Group 1 group: name: "{{ group_name1 }}" - inventory: "{{ result.id }}" + inventory: "{{ inv_result.id }}" state: present variables: foo: bar @@ -29,7 +58,46 @@ that: - "result is changed" -- name: Create a Group +- name: Create Group 1 with exists + group: + name: "{{ group_name1 }}" + inventory: "{{ inv_name }}" + state: exists + variables: + foo: bar + register: result + +- assert: + that: + - "result is not changed" + +- name: Delete Group 1 + group: + name: "{{ group_name1 }}" + inventory: "{{ inv_name }}" + state: absent + variables: + foo: bar + register: result + +- assert: + that: + - "result is changed" + +- name: Create Group 1 with exists + group: + name: "{{ group_name1 }}" + inventory: "{{ inv_name }}" + state: exists + variables: + foo: bar + register: result + +- assert: + that: + - "result is changed" + +- name: Create Group 2 group: name: "{{ group_name2 }}" inventory: "{{ inv_name }}" @@ -42,7 +110,7 @@ that: - "result is changed" -- name: Create a Group +- name: Create Group 3 group: name: "{{ group_name3 }}" inventory: "{{ inv_name }}" @@ -64,7 +132,7 @@ - "{{ host_name2 }}" - "{{ host_name3 }}" -- name: Create a Group with hosts and sub group +- name: Create Group 1 with hosts and sub group of Group 2 group: name: "{{ group_name1 }}" inventory: "{{ inv_name }}" @@ -78,7 +146,7 @@ foo: bar register: result -- name: Create a Group with hosts and sub group +- name: Create Group 1 with hosts and sub group group: name: "{{ group_name1 }}" inventory: "{{ inv_name }}" @@ -99,9 +167,9 @@ that: - group1_host_count == "3" -- name: Delete a Group +- name: Delete Group 3 group: - name: "{{ group_name1 }}" + name: "{{ group_name3 }}" inventory: "{{ inv_name }}" state: absent register: result @@ -110,9 +178,10 @@ that: - "result is changed" -- name: Delete a Group +# If we delete group 1 first it will delete group 2 and 3 +- name: Delete Group 1 group: - name: "{{ group_name2 }}" + name: "{{ group_name1 }}" inventory: "{{ inv_name }}" state: absent register: result @@ -121,13 +190,14 @@ that: - "result is changed" -- name: Delete a Group +- name: Delete Group 2 group: - name: "{{ group_name3 }}" + name: "{{ group_name2 }}" inventory: "{{ inv_name }}" state: absent register: result +# In this case, group 2 was last a child of group1 so deleting group1 deleted group2 - assert: that: - "result is not changed" diff --git a/ansible_collections/awx/awx/tests/integration/targets/host/tasks/main.yml b/ansible_collections/awx/awx/tests/integration/targets/host/tasks/main.yml index a0321b09c..2b5d66ff8 100644 --- a/ansible_collections/awx/awx/tests/integration/targets/host/tasks/main.yml +++ b/ansible_collections/awx/awx/tests/integration/targets/host/tasks/main.yml @@ -1,8 +1,13 @@ --- +- name: Generate a test ID + set_fact: + test_id: "{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}" + when: test_id is not defined + - name: Generate names set_fact: - host_name: "AWX-Collection-tests-host-host-{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}" - inv_name: "AWX-Collection-tests-host-inv-{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}" + host_name: "AWX-Collection-tests-host-host-{{ test_id }}" + inv_name: "AWX-Collection-tests-host-inv-{{ test_id }}" - name: Create an Inventory inventory: @@ -24,6 +29,64 @@ that: - "result is changed" +- name: Create a Host with exists + host: + name: "{{ host_name }}" + inventory: "{{ inv_name }}" + state: exists + variables: + foo: bar + register: result + +- assert: + that: + - "result is not changed" + +- name: Modify the host as a no-op + host: + name: "{{ host_name }}" + inventory: "{{ inv_name }}" + register: result + +- assert: + that: + - "result is not changed" + +- name: Delete a Host + host: + name: "{{ host_name }}" + inventory: "{{ inv_name }}" + state: absent + variables: + foo: bar + register: result + +- assert: + that: + - "result is changed" + +- name: Create a Host with exists + host: + name: "{{ host_name }}" + inventory: "{{ inv_name }}" + state: exists + variables: + foo: bar + register: result + +- assert: + that: + - "result is changed" + +- name: Use lookup to check that host was enabled + ansible.builtin.set_fact: + host_enabled_test: "lookup('awx.awx.controller_api', 'hosts/{{result.id}}/').enabled" + +- name: Newly created host should have API default value for enabled + assert: + that: + - host_enabled_test + - name: Delete a Host host: name: "{{ result.id }}" diff --git a/ansible_collections/awx/awx/tests/integration/targets/instance/tasks/main.yml b/ansible_collections/awx/awx/tests/integration/targets/instance/tasks/main.yml index e312c5fac..045567b50 100644 --- a/ansible_collections/awx/awx/tests/integration/targets/instance/tasks/main.yml +++ b/ansible_collections/awx/awx/tests/integration/targets/instance/tasks/main.yml @@ -1,14 +1,25 @@ --- +- name: Generate a test ID + set_fact: + test_id: "{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}" + when: test_id is not defined + - name: Generate hostnames set_fact: - hostname1: "AWX-Collection-tests-instance1.{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}.example.com" - hostname2: "AWX-Collection-tests-instance2.{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}.example.com" - hostname3: "AWX-Collection-tests-instance3.{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}.example.com" + hostname1: "AWX-Collection-tests-instance1.{{ test_id }}.example.com" + hostname2: "AWX-Collection-tests-instance2.{{ test_id }}.example.com" + hostname3: "AWX-Collection-tests-instance3.{{ test_id }}.example.com" register: facts -- name: Show hostnames - debug: - var: facts +- name: Get the k8s setting + set_fact: + IS_K8S: "{{ controller_settings['IS_K8S'] | default(False) }}" + vars: + controller_settings: "{{ lookup('awx.awx.controller_api', 'settings/all') }}" + +- debug: + msg: "Skipping instance test since this is instance is not running on a K8s platform" + when: not IS_K8S - block: - name: Create an instance @@ -31,7 +42,6 @@ node_type: execution node_state: installed capacity_adjustment: 0.4 - listener_port: 31337 register: result - assert: @@ -57,3 +67,67 @@ - "{{ hostname1 }}" - "{{ hostname2 }}" - "{{ hostname3 }}" + + when: IS_K8S + +- block: + - name: Create hop node 1 + awx.awx.instance: + hostname: "{{ hostname1 }}" + node_type: hop + node_state: installed + register: result + + - assert: + that: + - result is changed + + - name: Create hop node 2 + awx.awx.instance: + hostname: "{{ hostname2 }}" + node_type: hop + node_state: installed + register: result + + - assert: + that: + - result is changed + + - name: Create execution node + awx.awx.instance: + hostname: "{{ hostname3 }}" + node_type: execution + node_state: installed + peers: + - "{{ hostname1 }}" + - "{{ hostname2 }}" + register: result + + - assert: + that: + - result is changed + + - name: Remove execution node peers + awx.awx.instance: + hostname: "{{ hostname3 }}" + node_type: execution + node_state: installed + peers: [] + register: result + + - assert: + that: + - result is changed + + always: + - name: Deprovision the instances + awx.awx.instance: + hostname: "{{ item }}" + node_state: deprovisioning + with_items: + - "{{ hostname1 }}" + - "{{ hostname2 }}" + - "{{ hostname3 }}" + + + when: IS_K8S diff --git a/ansible_collections/awx/awx/tests/integration/targets/instance_group/tasks/main.yml b/ansible_collections/awx/awx/tests/integration/targets/instance_group/tasks/main.yml index 701137f28..1ef9d2da3 100644 --- a/ansible_collections/awx/awx/tests/integration/targets/instance_group/tasks/main.yml +++ b/ansible_collections/awx/awx/tests/integration/targets/instance_group/tasks/main.yml @@ -2,6 +2,7 @@ - name: Generate test id set_fact: test_id: "{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}" + when: test_id is not defined - name: Generate names set_fact: @@ -37,6 +38,42 @@ that: - "result is changed" + - name: Create an Instance Group with exists + instance_group: + name: "{{ group_name1 }}" + policy_instance_percentage: 34 + policy_instance_minimum: 12 + state: exists + register: result + + - assert: + that: + - "result is not changed" + + - name: Delete an Instance Group + instance_group: + name: "{{ group_name1 }}" + policy_instance_percentage: 34 + policy_instance_minimum: 12 + state: absent + register: result + + - assert: + that: + - "result is changed" + + - name: Create an Instance Group with exists + instance_group: + name: "{{ group_name1 }}" + policy_instance_percentage: 34 + policy_instance_minimum: 12 + state: exists + register: result + + - assert: + that: + - "result is changed" + - name: Update an Instance Group instance_group: name: "{{ result.id }}" diff --git a/ansible_collections/awx/awx/tests/integration/targets/inventory/tasks/main.yml b/ansible_collections/awx/awx/tests/integration/targets/inventory/tasks/main.yml index abbe4f659..dbaf7dbce 100644 --- a/ansible_collections/awx/awx/tests/integration/targets/inventory/tasks/main.yml +++ b/ansible_collections/awx/awx/tests/integration/targets/inventory/tasks/main.yml @@ -2,6 +2,7 @@ - name: Generate a test ID set_fact: test_id: "{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}" + when: test_id is not defined - name: Generate names set_fact: @@ -50,6 +51,45 @@ that: - "result is changed" + - name: Create an Inventory with exists + inventory: + name: "{{ inv_name1 }}" + organization: Default + instance_groups: + - "{{ group_name1 }}" + state: exists + register: result + + - assert: + that: + - "result is not changed" + + - name: Delete an Inventory + inventory: + name: "{{ inv_name1 }}" + organization: Default + instance_groups: + - "{{ group_name1 }}" + state: absent + register: result + + - assert: + that: + - "result is changed" + + - name: Create an Inventory with exists + inventory: + name: "{{ inv_name1 }}" + organization: Default + instance_groups: + - "{{ group_name1 }}" + state: exists + register: result + + - assert: + that: + - "result is changed" + - name: Test Inventory module idempotency inventory: name: "{{ result.id }}" diff --git a/ansible_collections/awx/awx/tests/integration/targets/inventory_source/tasks/main.yml b/ansible_collections/awx/awx/tests/integration/targets/inventory_source/tasks/main.yml index d905d03a9..a0442eeaf 100644 --- a/ansible_collections/awx/awx/tests/integration/targets/inventory_source/tasks/main.yml +++ b/ansible_collections/awx/awx/tests/integration/targets/inventory_source/tasks/main.yml @@ -1,9 +1,14 @@ --- +- name: Generate a test ID + set_fact: + test_id: "{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}" + when: test_id is not defined + - name: Generate names set_fact: - openstack_cred: "AWX-Collection-tests-inventory_source-cred-openstack-{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}" - openstack_inv: "AWX-Collection-tests-inventory_source-inv-openstack-{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}" - openstack_inv_source: "AWX-Collection-tests-inventory_source-inv-source-openstack-{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}" + openstack_cred: "AWX-Collection-tests-inventory_source-cred-openstack-{{ test_id }}" + openstack_inv: "AWX-Collection-tests-inventory_source-inv-openstack-{{ test_id }}" + openstack_inv_source: "AWX-Collection-tests-inventory_source-inv-source-openstack-{{ test_id }}" - name: Add a credential credential: @@ -25,7 +30,60 @@ organization: Default name: "{{ openstack_inv }}" -- name: Create a source inventory +- name: Create an source inventory + inventory_source: + name: "{{ openstack_inv_source }}" + description: Source for Test inventory + inventory: "{{ openstack_inv }}" + credential: "{{ credential_result.id }}" + overwrite: true + update_on_launch: true + source_vars: + private: false + source: openstack + register: result + +- assert: + that: + - "result is changed" + +- name: Create an source inventory with exists + inventory_source: + name: "{{ openstack_inv_source }}" + description: Source for Test inventory + inventory: "{{ openstack_inv }}" + credential: "{{ credential_result.id }}" + overwrite: true + update_on_launch: true + source_vars: + private: false + source: openstack + state: exists + register: result + +- assert: + that: + - "result is not changed" + +- name: Delete an source inventory + inventory_source: + name: "{{ openstack_inv_source }}" + description: Source for Test inventory + inventory: "{{ openstack_inv }}" + credential: "{{ credential_result.id }}" + overwrite: true + update_on_launch: true + source_vars: + private: false + source: openstack + state: absent + register: result + +- assert: + that: + - "result is changed" + +- name: Create an source inventory with exists inventory_source: name: "{{ openstack_inv_source }}" description: Source for Test inventory @@ -36,6 +94,7 @@ source_vars: private: false source: openstack + state: exists register: result - assert: diff --git a/ansible_collections/awx/awx/tests/integration/targets/inventory_source_update/tasks/main.yml b/ansible_collections/awx/awx/tests/integration/targets/inventory_source_update/tasks/main.yml index bc9182bb6..43d57c906 100644 --- a/ansible_collections/awx/awx/tests/integration/targets/inventory_source_update/tasks/main.yml +++ b/ansible_collections/awx/awx/tests/integration/targets/inventory_source_update/tasks/main.yml @@ -2,6 +2,7 @@ - name: Generate a test ID set_fact: test_id: "{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}" + when: test_id is not defined - name: Generate names set_fact: @@ -25,7 +26,7 @@ name: "{{ project_name }}" organization: "{{ org_name }}" scm_type: git - scm_url: https://github.com/ansible/test-playbooks + scm_url: https://github.com/ansible/ansible-tower-samples wait: true - name: Create a git project with same name, different org diff --git a/ansible_collections/awx/awx/tests/integration/targets/job_cancel/tasks/main.yml b/ansible_collections/awx/awx/tests/integration/targets/job_cancel/tasks/main.yml index 254ea89ce..deaab76f0 100644 --- a/ansible_collections/awx/awx/tests/integration/targets/job_cancel/tasks/main.yml +++ b/ansible_collections/awx/awx/tests/integration/targets/job_cancel/tasks/main.yml @@ -11,6 +11,7 @@ - name: Cancel the job job_cancel: job_id: "{{ job.id }}" + request_timeout: 60 register: results - assert: @@ -23,10 +24,10 @@ fail_if_not_running: true register: results ignore_errors: true - -- assert: - that: - - results is failed + # This test can be flaky, so we retry it a few times + until: results is failed and results.msg == 'Job is not running' + retries: 6 + delay: 5 - name: Check module fails with correct msg job_cancel: diff --git a/ansible_collections/awx/awx/tests/integration/targets/job_launch/tasks/main.yml b/ansible_collections/awx/awx/tests/integration/targets/job_launch/tasks/main.yml index 843c5c96f..23e43cf42 100644 --- a/ansible_collections/awx/awx/tests/integration/targets/job_launch/tasks/main.yml +++ b/ansible_collections/awx/awx/tests/integration/targets/job_launch/tasks/main.yml @@ -1,9 +1,14 @@ --- +- name: Generate a test ID + set_fact: + test_id: "{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}" + when: test_id is not defined + - name: Generate names set_fact: - jt_name1: "AWX-Collection-tests-job_launch-jt1-{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}" - jt_name2: "AWX-Collection-tests-job_launch-jt2-{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}" - proj_name: "AWX-Collection-tests-job_launch-project-{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}" + jt_name1: "AWX-Collection-tests-job_launch-jt1-{{ test_id }}" + jt_name2: "AWX-Collection-tests-job_launch-jt2-{{ test_id }}" + proj_name: "AWX-Collection-tests-job_launch-project-{{ test_id }}" - name: Launch a Job Template job_launch: diff --git a/ansible_collections/awx/awx/tests/integration/targets/job_list/tasks/main.yml b/ansible_collections/awx/awx/tests/integration/targets/job_list/tasks/main.yml index 04495bfcb..b9e602cb4 100644 --- a/ansible_collections/awx/awx/tests/integration/targets/job_list/tasks/main.yml +++ b/ansible_collections/awx/awx/tests/integration/targets/job_list/tasks/main.yml @@ -16,7 +16,7 @@ - assert: that: - - "{{ matching_jobs.count }} == 1" + - matching_jobs.count == 1 - name: List failed jobs (which don't exist) job_list: @@ -26,7 +26,7 @@ - assert: that: - - "{{ successful_jobs.count }} == 0" + - successful_jobs.count == 0 - name: Get ALL result pages! job_list: diff --git a/ansible_collections/awx/awx/tests/integration/targets/job_template/tasks/main.yml b/ansible_collections/awx/awx/tests/integration/targets/job_template/tasks/main.yml index 951fe27f9..b8e513009 100644 --- a/ansible_collections/awx/awx/tests/integration/targets/job_template/tasks/main.yml +++ b/ansible_collections/awx/awx/tests/integration/targets/job_template/tasks/main.yml @@ -2,6 +2,7 @@ - name: Generate a random string for test set_fact: test_id: "{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}" + when: test_id is not defined - name: generate random string for project set_fact: @@ -9,6 +10,7 @@ cred1: "AWX-Collection-tests-job_template-cred1-{{ test_id }}" cred2: "AWX-Collection-tests-job_template-cred2-{{ test_id }}" cred3: "AWX-Collection-tests-job_template-cred3-{{ test_id }}" + inv1: "AWX-Collection-tests-job_template-inv-{{ test_id }}" proj1: "AWX-Collection-tests-job_template-proj-{{ test_id }}" jt1: "AWX-Collection-tests-job_template-jt1-{{ test_id }}" jt2: "AWX-Collection-tests-job_template-jt2-{{ test_id }}" @@ -24,6 +26,11 @@ - Ansible Galaxy register: result +- name: Create an inventory + inventory: + name: "{{ inv1 }}" + organization: "{{ org_name }}" + - name: Create a Demo Project project: name: "{{ proj1 }}" @@ -103,7 +110,7 @@ job_template: name: "{{ jt1 }}" project: "{{ proj1 }}" - inventory: Demo Inventory + inventory: "{{ inv1 }}" playbook: hello_world.yml credentials: - "{{ cred1 }}" @@ -118,6 +125,63 @@ that: - "jt1_result is changed" +- name: Create Job Template 1 with exists + job_template: + name: "{{ jt1 }}" + project: "{{ proj1 }}" + inventory: "{{ inv1 }}" + playbook: hello_world.yml + credentials: + - "{{ cred1 }}" + - "{{ cred2 }}" + instance_groups: + - "{{ group_name1 }}" + job_type: run + state: exists + register: jt1_result + +- assert: + that: + - "jt1_result is not changed" + +- name: Delete Job Template 1 + job_template: + name: "{{ jt1 }}" + project: "{{ proj1 }}" + inventory: "{{ inv1 }}" + playbook: hello_world.yml + credentials: + - "{{ cred1 }}" + - "{{ cred2 }}" + instance_groups: + - "{{ group_name1 }}" + job_type: run + state: absent + register: jt1_result + +- assert: + that: + - "jt1_result is changed" + +- name: Create Job Template 1 with exists + job_template: + name: "{{ jt1 }}" + project: "{{ proj1 }}" + inventory: "{{ inv1 }}" + playbook: hello_world.yml + credentials: + - "{{ cred1 }}" + - "{{ cred2 }}" + instance_groups: + - "{{ group_name1 }}" + job_type: run + state: exists + register: jt1_result + +- assert: + that: + - "jt1_result is changed" + - name: Add a credential to this JT job_template: name: "{{ jt1 }}" @@ -217,7 +281,7 @@ name: "{{ jt2 }}" organization: Default project: "{{ proj1 }}" - inventory: Demo Inventory + inventory: "{{ inv1 }}" playbook: hello_world.yml credential: "{{ cred3 }}" job_type: run @@ -235,7 +299,7 @@ name: "{{ jt2 }}" organization: Default project: "{{ proj1 }}" - inventory: Demo Inventory + inventory: "{{ inv1 }}" playbook: hello_world.yml credential: "{{ cred3 }}" job_type: run @@ -383,7 +447,7 @@ job_template: name: "{{ jt2 }}" project: "{{ proj1 }}" - inventory: Demo Inventory + inventory: "{{ inv1 }}" playbook: hello_world.yml credential: "{{ cred3 }}" job_type: run @@ -443,6 +507,12 @@ organization: Default state: absent +- name: Delete an inventory + inventory: + name: "{{ inv1 }}" + organization: "{{ org_name }}" + state: absent + - name: "Remove the organization" organization: name: "{{ org_name }}" diff --git a/ansible_collections/awx/awx/tests/integration/targets/job_wait/tasks/main.yml b/ansible_collections/awx/awx/tests/integration/targets/job_wait/tasks/main.yml index 0aac7f314..cc38e171d 100644 --- a/ansible_collections/awx/awx/tests/integration/targets/job_wait/tasks/main.yml +++ b/ansible_collections/awx/awx/tests/integration/targets/job_wait/tasks/main.yml @@ -1,171 +1,164 @@ --- -- name: Generate random string for template and project - set_fact: - jt_name: "AWX-Collection-tests-job_wait-long_running-{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}" - proj_name: "AWX-Collection-tests-job_wait-long_running-{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}" - -- name: Assure that the demo project exists - project: - name: "{{ proj_name }}" - scm_type: 'git' - scm_url: 'https://github.com/ansible/test-playbooks.git' - scm_update_on_launch: true - organization: Default - -- name: Create a job template - job_template: - name: "{{ jt_name }}" - playbook: "sleep.yml" - job_type: run - project: "{{ proj_name }}" - inventory: "Demo Inventory" - extra_vars: - sleep_interval: 300 - -- name: Validate that interval superceeds min/max - job_wait: - min_interval: 10 - max_interval: 20 - interval: 12 - job_id: "99999999" - register: result - ignore_errors: true - -- assert: - that: - - "result.msg =='Unable to wait on job 99999999; that ID does not exist.' or - 'min and max interval have been depricated, please use interval instead, interval will be set to 12'" - -- name: Check module fails with correct msg - job_wait: - job_id: "99999999" - register: result - ignore_errors: true - -- assert: - that: - - result is failed - - "result.msg =='Unable to wait, no job_id 99999999 found: The requested object could not be found.' or - 'Unable to wait on job 99999999; that ID does not exist.'" - -- name: Launch Demo Job Template (take happy path) - job_launch: - job_template: "Demo Job Template" - register: job - -- assert: - that: - - job is changed - -- name: Wait for the Job to finish - job_wait: - job_id: "{{ job.id }}" - register: wait_results - -# Make sure it worked and that we have some data in our results -- assert: - that: - - wait_results is successful - - "'elapsed' in wait_results" - - "'id' in wait_results" - -- name: Launch a long running job - job_launch: - job_template: "{{ jt_name }}" - register: job - -- assert: - that: - - job is changed - -- name: Timeout waiting for the job to complete - job_wait: - job_id: "{{ job.id }}" - timeout: 5 - ignore_errors: true - register: wait_results - -# Make sure that we failed and that we have some data in our results -- assert: - that: - - "wait_results.msg == 'Monitoring aborted due to timeout' or 'Timeout waiting for job to finish.'" - - "'id' in wait_results" - -- name: Async cancel the long running job - job_cancel: - job_id: "{{ job.id }}" - async: 3600 - poll: 0 - -- name: Wait for the job to exit on cancel - job_wait: - job_id: "{{ job.id }}" - register: wait_results - ignore_errors: true - -- assert: - that: - - wait_results is failed - - 'wait_results.status == "canceled"' - - "wait_results.msg == 'Job with id {{ job.id }} failed' or 'Job with id={{ job.id }} failed, error: Job failed.'" - -- name: Delete the job template - job_template: - name: "{{ jt_name }}" - playbook: "sleep.yml" - job_type: run - project: "{{ proj_name }}" - inventory: "Demo Inventory" - state: absent - -- name: Delete the project - project: - name: "{{ proj_name }}" - organization: Default - state: absent - -# workflow wait test -- name: Generate a random string for test +- name: Generate a test ID set_fact: - test_id1: "{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}" - when: test_id1 is not defined + test_id: "{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}" + when: test_id is not defined -- name: Generate names +- name: Generate random string for template and project set_fact: - wfjt_name2: "AWX-Collection-tests-workflow_launch--wfjt1-{{ test_id1 }}" - -- name: Create our workflow - workflow_job_template: - name: "{{ wfjt_name2 }}" - state: present - -- name: Add a node - workflow_job_template_node: - workflow_job_template: "{{ wfjt_name2 }}" - unified_job_template: "Demo Job Template" - identifier: leaf - register: new_node - -- name: Kick off a workflow - workflow_launch: - workflow_template: "{{ wfjt_name2 }}" - ignore_errors: true - register: workflow - -- name: Wait for the Workflow Job to finish - job_wait: - job_id: "{{ workflow.job_info.id }}" - job_type: "workflow_jobs" - register: wait_workflow_results - -# Make sure it worked and that we have some data in our results -- assert: - that: - - wait_workflow_results is successful - - "'elapsed' in wait_workflow_results" - - "'id' in wait_workflow_results" - -- name: Clean up test workflow - workflow_job_template: - name: "{{ wfjt_name2 }}" - state: absent + jt_name: "AWX-Collection-tests-job_wait-long_running-{{ test_id }}" + proj_name: "AWX-Collection-tests-job_wait-long_running-{{ test_id }}" + +- block: + - name: Create a project + project: + name: "{{ proj_name }}" + scm_type: 'git' + scm_url: 'https://github.com/ansible/test-playbooks.git' + scm_update_on_launch: true + organization: Default + + - name: Create a job template + job_template: + name: "{{ jt_name }}" + playbook: "sleep.yml" + job_type: run + project: "{{ proj_name }}" + inventory: "Demo Inventory" + extra_vars: + sleep_interval: 600 + + - name: Check module fails with correct msg + job_wait: + job_id: "99999999" + register: result + ignore_errors: true + + - assert: + that: + - result is failed + - "result.msg =='Unable to wait, no job_id 99999999 found: The requested object could not be found.' or + 'Unable to wait on job 99999999; that ID does not exist.'" + + - name: Launch Demo Job Template (take happy path) + job_launch: + job_template: "Demo Job Template" + register: job + + - assert: + that: + - job is changed + + - name: Wait for the Job to finish + job_wait: + job_id: "{{ job.id }}" + register: wait_results + + # Make sure it worked and that we have some data in our results + - assert: + that: + - wait_results is successful + - "'elapsed' in wait_results" + - "'id' in wait_results" + + - name: Launch a long running job + job_launch: + job_template: "{{ jt_name }}" + register: job + + - assert: + that: + - job is changed + + - name: Timeout waiting for the job to complete + job_wait: + job_id: "{{ job.id }}" + timeout: 5 + ignore_errors: true + register: wait_results + + # Make sure that we failed and that we have some data in our results + - assert: + that: + - "wait_results.msg == 'Monitoring aborted due to timeout' or 'Timeout waiting for job to finish.'" + - "'id' in wait_results" + + - name: Async cancel the long running job + job_cancel: + job_id: "{{ job.id }}" + async: 3600 + poll: 0 + + - name: Wait for the job to exit on cancel + job_wait: + job_id: "{{ job.id }}" + register: wait_results + ignore_errors: true + + - assert: + that: + - wait_results is failed + - 'wait_results.status == "canceled"' + - "'Job with id ~ job.id failed' or 'Job with id= ~ job.id failed, error: Job failed.' is in wait_results.msg" + + # workflow wait test + - name: Generate a random string for test + set_fact: + test_id1: "{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}" + when: test_id1 is not defined + + - name: Generate names + set_fact: + wfjt_name2: "AWX-Collection-tests-workflow_launch--wfjt1-{{ test_id1 }}" + + - name: Create our workflow + workflow_job_template: + name: "{{ wfjt_name2 }}" + state: present + + - name: Add a node + workflow_job_template_node: + workflow_job_template: "{{ wfjt_name2 }}" + unified_job_template: "Demo Job Template" + identifier: leaf + register: new_node + + - name: Kick off a workflow + workflow_launch: + workflow_template: "{{ wfjt_name2 }}" + ignore_errors: true + register: workflow + + - name: Wait for the Workflow Job to finish + job_wait: + job_id: "{{ workflow.job_info.id }}" + job_type: "workflow_jobs" + register: wait_workflow_results + + # Make sure it worked and that we have some data in our results + - assert: + that: + - wait_workflow_results is successful + - "'elapsed' in wait_workflow_results" + - "'id' in wait_workflow_results" + + always: + - name: Clean up test workflow + workflow_job_template: + name: "{{ wfjt_name2 }}" + state: absent + + - name: Delete the job template + job_template: + name: "{{ jt_name }}" + playbook: "sleep.yml" + job_type: run + project: "{{ proj_name }}" + inventory: "Demo Inventory" + state: absent + + - name: Delete the project + project: + name: "{{ proj_name }}" + organization: Default + state: absent diff --git a/ansible_collections/awx/awx/tests/integration/targets/label/tasks/main.yml b/ansible_collections/awx/awx/tests/integration/targets/label/tasks/main.yml index 0ac077f8c..a98594d5c 100644 --- a/ansible_collections/awx/awx/tests/integration/targets/label/tasks/main.yml +++ b/ansible_collections/awx/awx/tests/integration/targets/label/tasks/main.yml @@ -1,13 +1,34 @@ --- +- name: Generate a test ID + set_fact: + test_id: "{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}" + when: test_id is not defined + - name: Generate names set_fact: - label_name: "AWX-Collection-tests-label-label-{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}" + label_name: "AWX-Collection-tests-label-label-{{ test_id }}" - name: Create a Label label: name: "{{ label_name }}" organization: Default state: present + register: results + +- assert: + that: + - "results is changed" + +- name: Create a Label with exists + label: + name: "{{ label_name }}" + organization: Default + state: exists + register: results + +- assert: + that: + - "results is not changed" - name: Check module fails with correct msg label: diff --git a/ansible_collections/awx/awx/tests/integration/targets/lookup_api_plugin/tasks/main.yml b/ansible_collections/awx/awx/tests/integration/targets/lookup_api_plugin/tasks/main.yml index 5abed9dcd..6fac5a0bc 100644 --- a/ansible_collections/awx/awx/tests/integration/targets/lookup_api_plugin/tasks/main.yml +++ b/ansible_collections/awx/awx/tests/integration/targets/lookup_api_plugin/tasks/main.yml @@ -101,6 +101,9 @@ set_fact: users: "{{ query(plugin_name, 'users', query_params={ 'username__endswith': test_id, 'page_size': 2 }, return_ids=True ) }}" + - debug: + msg: "{{ users }}" + - name: Assert that user list has 2 ids only and that they are strings, not ints assert: that: @@ -129,7 +132,7 @@ - name: Get the ID of the first user created and verify that it is correct assert: - that: "{{ query(plugin_name, 'users', query_params={ 'username' : user_creation_results['results'][0]['item'] }, return_ids=True)[0] }} == {{ user_creation_results['results'][0]['id'] }}" + that: "query(plugin_name, 'users', query_params={ 'username' : user_creation_results['results'][0]['item'] }, return_ids=True)[0] == user_creation_results['results'][0]['id'] | string" - name: Try to get an ID of someone who does not exist set_fact: diff --git a/ansible_collections/awx/awx/tests/integration/targets/lookup_rruleset/tasks/main.yml b/ansible_collections/awx/awx/tests/integration/targets/lookup_rruleset/tasks/main.yml index 7f9f0271b..28ad1da96 100644 --- a/ansible_collections/awx/awx/tests/integration/targets/lookup_rruleset/tasks/main.yml +++ b/ansible_collections/awx/awx/tests/integration/targets/lookup_rruleset/tasks/main.yml @@ -11,7 +11,7 @@ - name: Call ruleset with no rules set_fact: - complex_rule: "{{ query(ruleset_plugin_name, '2022-04-30 10:30:45') }}" + complex_rule: "{{ query(ruleset_plugin_name | string, '2022-04-30 10:30:45') }}" ignore_errors: True register: results @@ -356,3 +356,19 @@ that: - results is success - "'DTSTART;TZID=UTC:20220430T103045 RRULE:FREQ=MONTHLY;BYMONTHDAY=12,13,14,15,16,17,18;BYDAY=SA;INTERVAL=1' == complex_rule" + +- name: mondays, Tuesdays, and WEDNESDAY with case-insensitivity + set_fact: + complex_rule: "{{ query(ruleset_plugin_name, '2022-04-30 10:30:45', rules=rrules, timezone='UTC' ) }}" + ignore_errors: True + register: results + vars: + rrules: + - frequency: 'day' + interval: 1 + byweekday: monday, Tuesday, WEDNESDAY + +- assert: + that: + - results is success + - "'DTSTART;TZID=UTC:20220430T103045 RRULE:FREQ=DAILY;BYDAY=MO,TU,WE;INTERVAL=1' == complex_rule" diff --git a/ansible_collections/awx/awx/tests/integration/targets/module_utils/tasks/main.yml b/ansible_collections/awx/awx/tests/integration/targets/module_utils/tasks/main.yml new file mode 100644 index 000000000..2c0908b48 --- /dev/null +++ b/ansible_collections/awx/awx/tests/integration/targets/module_utils/tasks/main.yml @@ -0,0 +1,8 @@ +--- +- name: Generate a random string for test + set_fact: + test_id: "{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}" + when: test_id is not defined + +- include_tasks: + file: test_named_reference.yml diff --git a/ansible_collections/awx/awx/tests/integration/targets/module_utils/tasks/test_named_reference.yml b/ansible_collections/awx/awx/tests/integration/targets/module_utils/tasks/test_named_reference.yml new file mode 100644 index 000000000..3ae913a57 --- /dev/null +++ b/ansible_collections/awx/awx/tests/integration/targets/module_utils/tasks/test_named_reference.yml @@ -0,0 +1,73 @@ +--- +- block: + - name: generate random string for project + set_fact: + org_name: "AWX-Collection-tests-organization-org-{{ test_id }}" + cred: "AWX-Collection-tests-job_template-cred-{{ test_id }}" + inv: "AWX-Collection-tests-job_template-inv-{{ test_id }}" + proj: "AWX-Collection-tests-job_template-proj-{{ test_id }}" + jt: "AWX-Collection-tests-job_template-jt-{{ test_id }}" + + - name: "Create a new organization" + organization: + name: "{{ org_name }}" + galaxy_credentials: + - Ansible Galaxy + + - name: Create an inventory + inventory: + name: "{{ inv }}" + organization: "{{ org_name }}" + + - name: Create a Demo Project + project: + name: "{{ proj }}" + organization: "{{ org_name }}" + state: present + scm_type: git + scm_url: https://github.com/ansible/ansible-tower-samples.git + + - name: Create Credential + credential: + name: "{{ cred }}" + organization: "{{ org_name }}" + credential_type: Machine + + - name: Create Job Template + job_template: + name: "{{ jt }}" + project: "{{ proj }}++{{ org_name }}" + inventory: "{{ inv }}++{{ org_name }}" + playbook: hello_world.yml + credentials: + - "{{ cred }}++Machine+ssh++" + job_type: run + state: present + + always: + - name: Delete the Job Template + job_template: + name: "{{ jt }}" + state: absent + + - name: Delete the Demo Project + project: + name: "{{ proj }}++{{ org_name }}" + state: absent + + - name: Delete Credential + credential: + name: "{{ cred }}++Machine+ssh++{{ org_name }}" + credential_type: Machine + state: absent + + - name: Delete the inventory + inventory: + name: "{{ inv }}++{{ org_name }}" + organization: "{{ org_name }}" + state: absent + + - name: Remove the organization + organization: + name: "{{ org_name }}" + state: absent diff --git a/ansible_collections/awx/awx/tests/integration/targets/notification_template/tasks/main.yml b/ansible_collections/awx/awx/tests/integration/targets/notification_template/tasks/main.yml index 278845696..483024e2f 100644 --- a/ansible_collections/awx/awx/tests/integration/targets/notification_template/tasks/main.yml +++ b/ansible_collections/awx/awx/tests/integration/targets/notification_template/tasks/main.yml @@ -1,12 +1,17 @@ --- +- name: Generate a test ID + set_fact: + test_id: "{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}" + when: test_id is not defined + - name: Generate names set_fact: - slack_not: "AWX-Collection-tests-notification_template-slack-not-{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}" - webhook_not: "AWX-Collection-tests-notification_template-wehbook-not-{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}" - email_not: "AWX-Collection-tests-notification_template-email-not-{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}" - twillo_not: "AWX-Collection-tests-notification_template-twillo-not-{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}" - pd_not: "AWX-Collection-tests-notification_template-pd-not-{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}" - irc_not: "AWX-Collection-tests-notification_template-irc-not-{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}" + slack_not: "AWX-Collection-tests-notification_template-slack-not-{{ test_id }}" + webhook_not: "AWX-Collection-tests-notification_template-wehbook-not-{{ test_id }}" + email_not: "AWX-Collection-tests-notification_template-email-not-{{ test_id }}" + twillo_not: "AWX-Collection-tests-notification_template-twillo-not-{{ test_id }}" + pd_not: "AWX-Collection-tests-notification_template-pd-not-{{ test_id }}" + irc_not: "AWX-Collection-tests-notification_template-irc-not-{{ test_id }}" - name: Create Slack notification with custom messages notification_template: @@ -31,6 +36,75 @@ that: - result is changed +- name: Create Slack notification with custom messages with exists + notification_template: + name: "{{ slack_not }}" + organization: Default + notification_type: slack + notification_configuration: + token: a_token + channels: + - general + messages: + started: + message: "{{ '{{' }} job_friendly_name {{' }}' }} {{ '{{' }} job.id {{' }}' }} started" + success: + message: "{{ '{{' }} job_friendly_name {{ '}}' }} completed in {{ '{{' }} job.elapsed {{ '}}' }} seconds" + error: + message: "{{ '{{' }} job_friendly_name {{ '}}' }} FAILED! Please look at {{ '{{' }} job.url {{ '}}' }}" + state: exists + register: result + +- assert: + that: + - result is not changed + +- name: Delete Slack notification with custom messages + notification_template: + name: "{{ slack_not }}" + organization: Default + notification_type: slack + notification_configuration: + token: a_token + channels: + - general + messages: + started: + message: "{{ '{{' }} job_friendly_name {{' }}' }} {{ '{{' }} job.id {{' }}' }} started" + success: + message: "{{ '{{' }} job_friendly_name {{ '}}' }} completed in {{ '{{' }} job.elapsed {{ '}}' }} seconds" + error: + message: "{{ '{{' }} job_friendly_name {{ '}}' }} FAILED! Please look at {{ '{{' }} job.url {{ '}}' }}" + state: absent + register: result + +- assert: + that: + - result is changed + +- name: Create Slack notification with custom messages with exists + notification_template: + name: "{{ slack_not }}" + organization: Default + notification_type: slack + notification_configuration: + token: a_token + channels: + - general + messages: + started: + message: "{{ '{{' }} job_friendly_name {{' }}' }} {{ '{{' }} job.id {{' }}' }} started" + success: + message: "{{ '{{' }} job_friendly_name {{ '}}' }} completed in {{ '{{' }} job.elapsed {{ '}}' }} seconds" + error: + message: "{{ '{{' }} job_friendly_name {{ '}}' }} FAILED! Please look at {{ '{{' }} job.url {{ '}}' }}" + state: exists + register: result + +- assert: + that: + - result is changed + - name: Delete Slack notification notification_template: name: "{{ slack_not }}" diff --git a/ansible_collections/awx/awx/tests/integration/targets/organization/tasks/main.yml b/ansible_collections/awx/awx/tests/integration/targets/organization/tasks/main.yml index fcf34e472..57af83c47 100644 --- a/ansible_collections/awx/awx/tests/integration/targets/organization/tasks/main.yml +++ b/ansible_collections/awx/awx/tests/integration/targets/organization/tasks/main.yml @@ -2,6 +2,7 @@ - name: Generate a test ID set_fact: test_id: "{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}" + when: test_id is not defined - name: Generate an org name set_fact: @@ -24,6 +25,39 @@ - assert: that: "result is changed" +- name: "Create a new organization with exists" + organization: + name: "{{ org_name }}" + galaxy_credentials: + - Ansible Galaxy + state: exists + register: result + +- assert: + that: "result is not changed" + +- name: "Delete a new organization" + organization: + name: "{{ org_name }}" + galaxy_credentials: + - Ansible Galaxy + state: absent + register: result + +- assert: + that: "result is changed" + +- name: "Create a new organization with exists" + organization: + name: "{{ org_name }}" + galaxy_credentials: + - Ansible Galaxy + state: exists + register: result + +- assert: + that: "result is changed" + - name: "Make sure making the same org is not a change" organization: name: "{{ org_name }}" diff --git a/ansible_collections/awx/awx/tests/integration/targets/project/tasks/main.yml b/ansible_collections/awx/awx/tests/integration/targets/project/tasks/main.yml index 3a8889ee2..a5a059beb 100644 --- a/ansible_collections/awx/awx/tests/integration/targets/project/tasks/main.yml +++ b/ansible_collections/awx/awx/tests/integration/targets/project/tasks/main.yml @@ -1,13 +1,18 @@ --- +- name: Generate a test ID + set_fact: + test_id: "{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}" + when: test_id is not defined + - name: Generate names set_fact: - project_name1: "AWX-Collection-tests-project-project1-{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}" - project_name2: "AWX-Collection-tests-project-project2-{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}" - project_name3: "AWX-Collection-tests-project-project3-{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}" - jt1: "AWX-Collection-tests-project-jt1-{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}" - scm_cred_name: "AWX-Collection-tests-project-scm-cred-{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}" - org_name: "AWX-Collection-tests-project-org-{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}" - cred_name: "AWX-Collection-tests-project-cred-{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}" + project_name1: "AWX-Collection-tests-project-project1-{{ test_id }}" + project_name2: "AWX-Collection-tests-project-project2-{{ test_id }}" + project_name3: "AWX-Collection-tests-project-project3-{{ test_id }}" + jt1: "AWX-Collection-tests-project-jt1-{{ test_id }}" + scm_cred_name: "AWX-Collection-tests-project-scm-cred-{{ test_id }}" + org_name: "AWX-Collection-tests-project-org-{{ test_id }}" + cred_name: "AWX-Collection-tests-project-cred-{{ test_id }}" - block: - name: Create an SCM Credential @@ -26,8 +31,67 @@ name: "{{ project_name1 }}" organization: Default scm_type: git - scm_url: https://github.com/ansible/test-playbooks + scm_url: https://github.com/ansible/ansible-tower-samples + wait: true + register: result + + - assert: + that: + - result is changed + + - name: Create a git project without credentials and wait with exists + project: + name: "{{ project_name1 }}" + organization: Default + scm_type: git + scm_url: https://github.com/ansible/ansible-tower-samples + wait: true + state: exists + register: result + + - assert: + that: + - result is not changed + + - name: Create a git project and wait with short request timeout. + project: + name: "{{ project_name1 }}" + organization: Default + scm_type: git + scm_url: https://github.com/ansible/ansible-tower-samples + wait: true + state: exists + request_timeout: .001 + register: result + ignore_errors: true + + - assert: + that: + - result is failed + - "'timed out' in result.msg" + + - name: Delete a git project without credentials and wait + project: + name: "{{ project_name1 }}" + organization: Default + scm_type: git + scm_url: https://github.com/ansible/ansible-tower-samples + wait: true + state: absent + register: result + + - assert: + that: + - result is changed + + - name: Create a git project without credentials and wait with exists + project: + name: "{{ project_name1 }}" + organization: Default + scm_type: git + scm_url: https://github.com/ansible/ansible-tower-samples wait: true + state: exists register: result - assert: @@ -39,7 +103,7 @@ name: "{{ project_name1 }}" organization: Default scm_type: git - scm_url: https://github.com/ansible/test-playbooks + scm_url: https://github.com/ansible/ansible-tower-samples wait: false register: result ignore_errors: true @@ -73,7 +137,7 @@ name: "{{ project_name2 }}" organization: "{{ org_name }}" scm_type: git - scm_url: https://github.com/ansible/test-playbooks + scm_url: https://github.com/ansible/ansible-tower-samples scm_credential: "{{ cred_name }}" check_mode: true @@ -98,7 +162,7 @@ name: "{{ project_name2 }}" organization: Non_Existing_Org scm_type: git - scm_url: https://github.com/ansible/test-playbooks + scm_url: https://github.com/ansible/ansible-tower-samples scm_credential: "{{ cred_name }}" register: result ignore_errors: true @@ -115,7 +179,7 @@ name: "{{ project_name2 }}" organization: "{{ org_name }}" scm_type: git - scm_url: https://github.com/ansible/test-playbooks + scm_url: https://github.com/ansible/ansible-tower-samples scm_credential: Non_Existing_Credential register: result ignore_errors: true @@ -127,7 +191,7 @@ - "'Non_Existing_Credential' in result.msg" - "result.total_results == 0" - - name: Create a git project without credentials without waiting + - name: Create a git project using a branch and allowing branch override project: name: "{{ project_name3 }}" organization: Default diff --git a/ansible_collections/awx/awx/tests/integration/targets/project_manual/tasks/create_project_dir.yml b/ansible_collections/awx/awx/tests/integration/targets/project_manual/tasks/create_project_dir.yml deleted file mode 100644 index 9fb960725..000000000 --- a/ansible_collections/awx/awx/tests/integration/targets/project_manual/tasks/create_project_dir.yml +++ /dev/null @@ -1,56 +0,0 @@ ---- -- name: Load the UI settings - set_fact: - project_base_dir: "{{ controller_settings.project_base_dir }}" - vars: - controller_settings: "{{ lookup('awx.awx.controller_api', 'config/') }}" - -- inventory: - name: localhost - organization: Default - -- host: - name: localhost - inventory: localhost - variables: - ansible_connection: local - -- name: Create an unused SSH / Machine credential - credential: - name: dummy - credential_type: Machine - inputs: - ssh_key_data: | - -----BEGIN EC PRIVATE KEY----- - MHcCAQEEIIUl6R1xgzR6siIUArz4XBPtGZ09aetma2eWf1v3uYymoAoGCCqGSM49 - AwEHoUQDQgAENJNjgeZDAh/+BY860s0yqrLDprXJflY0GvHIr7lX3ieCtrzOMCVU - QWzw35pc5tvuP34SSi0ZE1E+7cVMDDOF3w== - -----END EC PRIVATE KEY----- - organization: Default - -- block: - - name: Add a path to a setting - settings: - name: AWX_ISOLATION_SHOW_PATHS - value: "[{{ project_base_dir }}]" - - - name: Create a directory for manual project - ad_hoc_command: - credential: dummy - inventory: localhost - job_type: run - module_args: "mkdir -p {{ project_base_dir }}/{{ project_dir_name }}" - module_name: command - wait: true - - always: - - name: Delete path from setting - settings: - name: AWX_ISOLATION_SHOW_PATHS - value: [] - - - name: Delete dummy credential - credential: - name: dummy - credential_type: Machine - state: absent diff --git a/ansible_collections/awx/awx/tests/integration/targets/project_manual/tasks/main.yml b/ansible_collections/awx/awx/tests/integration/targets/project_manual/tasks/main.yml deleted file mode 100644 index 3cd328b4f..000000000 --- a/ansible_collections/awx/awx/tests/integration/targets/project_manual/tasks/main.yml +++ /dev/null @@ -1,38 +0,0 @@ ---- -- name: Generate random string for project - set_fact: - rand_string: "{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}" - -- name: Generate manual project name - set_fact: - project_name: "Manual_Project_{{ rand_string }}" - -- name: Generate manual project dir name - set_fact: - project_dir_name: "proj_{{ rand_string }}" - -- name: Create a project directory for manual project - import_tasks: create_project_dir.yml - -- name: Create a manual project - project: - name: "{{ project_name }}" - organization: Default - scm_type: manual - local_path: "{{ project_dir_name }}" - register: result - -- assert: - that: - - "result is changed" - -- name: Delete a manual project - project: - name: "{{ project_name }}" - organization: Default - state: absent - register: result - -- assert: - that: - - "result is changed" diff --git a/ansible_collections/awx/awx/tests/integration/targets/project_update/tasks/main.yml b/ansible_collections/awx/awx/tests/integration/targets/project_update/tasks/main.yml index 4b08e685f..451fe4ff6 100644 --- a/ansible_collections/awx/awx/tests/integration/targets/project_update/tasks/main.yml +++ b/ansible_collections/awx/awx/tests/integration/targets/project_update/tasks/main.yml @@ -13,7 +13,7 @@ name: "{{ project_name1 }}" organization: Default scm_type: git - scm_url: https://github.com/ansible/test-playbooks + scm_url: https://github.com/ansible/ansible-tower-samples wait: false register: project_create_result @@ -61,6 +61,10 @@ organization: Default state: absent register: result + until: result is changed # wait for the project update to settle + retries: 6 + delay: 5 + - assert: that: diff --git a/ansible_collections/awx/awx/tests/integration/targets/role/tasks/main.yml b/ansible_collections/awx/awx/tests/integration/targets/role/tasks/main.yml index a94f53413..dd499b1d2 100644 --- a/ansible_collections/awx/awx/tests/integration/targets/role/tasks/main.yml +++ b/ansible_collections/awx/awx/tests/integration/targets/role/tasks/main.yml @@ -2,6 +2,7 @@ - name: Generate a test id set_fact: test_id: "{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}" + when: test_id is not defined - name: Generate names set_fact: @@ -10,6 +11,8 @@ jt1: "AWX-Collection-tests-role-jt1-{{ test_id }}" jt2: "AWX-Collection-tests-role-jt2-{{ test_id }}" wfjt_name: "AWX-Collection-tests-role-project-wfjt-{{ test_id }}" + team_name: "AWX-Collection-tests-team-team-{{ test_id }}" + team2_name: "AWX-Collection-tests-team-team-{{ test_id }}2" - block: - name: Create a User @@ -26,6 +29,32 @@ that: - "result is changed" + - name: Create a 2nd User + user: + first_name: Joe + last_name: User + username: "{{ username }}2" + password: "{{ 65535 | random | to_uuid }}" + email: joe@example.org + state: present + register: result + + - assert: + that: + - "result is changed" + + - name: Create teams + team: + name: "{{ item }}" + organization: Default + register: result + loop: + - "{{ team_name }}" + - "{{ team2_name }}" + - assert: + that: + - "result is changed" + - name: Create a project project: name: "{{ project_name }}" @@ -54,9 +83,14 @@ that: - "result is changed" - - name: Add Joe to the update role of the default Project with lookup Organization + - name: Add Joe and teams to the update role of the default Project with lookup Organization role: user: "{{ username }}" + users: + - "{{ username }}2" + teams: + - "{{ team_name }}" + - "{{ team2_name }}" role: update lookup_organization: Default project: "Demo Project" @@ -73,6 +107,11 @@ - name: Add Joe to the new project by ID role: user: "{{ username }}" + users: + - "{{ username }}2" + teams: + - "{{ team_name }}" + - "{{ team2_name }}" role: update project: "{{ project_info['id'] }}" state: "{{ item }}" @@ -88,6 +127,8 @@ - name: Add Joe as execution admin to Default Org. role: user: "{{ username }}" + users: + - "{{ username }}2" role: execution_environment_admin organizations: Default state: "{{ item }}" @@ -109,6 +150,8 @@ - name: Add Joe to workflow execute role role: user: "{{ username }}" + users: + - "{{ username }}2" role: execute workflow: test-role-workflow job_templates: @@ -124,6 +167,8 @@ - name: Add Joe to nonexistant job template execute role role: user: "{{ username }}" + users: + - "{{ username }}2" role: execute workflow: test-role-workflow job_templates: @@ -140,6 +185,8 @@ - name: Add Joe to workflow execute role, no-op role: user: "{{ username }}" + users: + - "{{ username }}2" role: execute workflow: test-role-workflow state: present @@ -151,7 +198,8 @@ - name: Add Joe to workflow approve role role: - user: "{{ username }}" + users: + - "{{ username }}2" role: approval workflow: test-role-workflow state: present @@ -169,6 +217,23 @@ state: absent register: result + - name: Delete a 2nd User + user: + username: "{{ username }}2" + email: joe@example.org + state: absent + register: result + + - name: Delete teams + team: + name: "{{ item }}" + organization: Default + state: absent + register: result + loop: + - "{{ team_name }}" + - "{{ team2_name }}" + - name: Delete job templates job_template: name: "{{ item }}" diff --git a/ansible_collections/awx/awx/tests/integration/targets/schedule/tasks/main.yml b/ansible_collections/awx/awx/tests/integration/targets/schedule/tasks/main.yml index 73343faf9..c61785d3b 100644 --- a/ansible_collections/awx/awx/tests/integration/targets/schedule/tasks/main.yml +++ b/ansible_collections/awx/awx/tests/integration/targets/schedule/tasks/main.yml @@ -2,6 +2,7 @@ - name: Generate a random string for test set_fact: test_id: "{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}" + when: test_id is not defined - name: generate random string for schedule set_fact: @@ -35,7 +36,7 @@ - assert: that: - result is failed - - "'Unable to create schedule {{ sched1 }}' in result.msg" + - "'Unable to create schedule '~ sched1 in result.msg" - name: Create with options that the JT does not support schedule: @@ -61,7 +62,7 @@ - assert: that: - result is failed - - "'Unable to create schedule {{ sched1 }}' in result.msg" + - "'Unable to create schedule '~ sched1 in result.msg" - name: Build a real schedule schedule: @@ -75,6 +76,51 @@ that: - result is changed + - name: Use lookup to check that schedules was enabled + ansible.builtin.set_fact: + schedules_enabled_test: "lookup('awx.awx.controller_api', 'schedules/{{result.id}}/').enabled" + + - name: Newly created schedules should have API default value for enabled + assert: + that: + - schedules_enabled_test + + - name: Build a real schedule with exists + schedule: + name: "{{ sched1 }}" + state: exists + unified_job_template: "Demo Job Template" + rrule: "DTSTART:20191219T130551Z RRULE:FREQ=WEEKLY;INTERVAL=1;COUNT=1" + register: result + + - assert: + that: + - result is not changed + + - name: Delete a real schedule + schedule: + name: "{{ sched1 }}" + state: absent + unified_job_template: "Demo Job Template" + rrule: "DTSTART:20191219T130551Z RRULE:FREQ=WEEKLY;INTERVAL=1;COUNT=1" + register: result + + - assert: + that: + - result is changed + + - name: Build a real schedule with exists + schedule: + name: "{{ sched1 }}" + state: exists + unified_job_template: "Demo Job Template" + rrule: "DTSTART:20191219T130551Z RRULE:FREQ=WEEKLY;INTERVAL=1;COUNT=1" + register: result + + - assert: + that: + - result is changed + - name: Rebuild the same schedule schedule: name: "{{ sched1 }}" @@ -188,6 +234,7 @@ schedule: name: "{{ sched2 }}" state: present + organization: Default unified_job_template: "{{ jt1 }}" rrule: "DTSTART:20191219T130551Z RRULE:FREQ=WEEKLY;INTERVAL=1;COUNT=1" description: "This hopefully will work" diff --git a/ansible_collections/awx/awx/tests/integration/targets/schedule_rrule/tasks/main.yml b/ansible_collections/awx/awx/tests/integration/targets/schedule_rrule/tasks/main.yml index bf416b813..af2d95300 100644 --- a/ansible_collections/awx/awx/tests/integration/targets/schedule_rrule/tasks/main.yml +++ b/ansible_collections/awx/awx/tests/integration/targets/schedule_rrule/tasks/main.yml @@ -9,7 +9,7 @@ - name: Test too many params (failure from validation of terms) debug: - msg: "{{ query(plugin_name, 'none', 'weekly', start_date='2020-4-16 03:45:07') }}" + msg: "{{ query(plugin_name | string, 'none', 'weekly', start_date='2020-4-16 03:45:07') }}" ignore_errors: true register: result diff --git a/ansible_collections/awx/awx/tests/integration/targets/settings/tasks/main.yml b/ansible_collections/awx/awx/tests/integration/targets/settings/tasks/main.yml index 65ae45c63..5feb9ee80 100644 --- a/ansible_collections/awx/awx/tests/integration/targets/settings/tasks/main.yml +++ b/ansible_collections/awx/awx/tests/integration/targets/settings/tasks/main.yml @@ -1,4 +1,42 @@ --- +- name: Initialize starting project vvv setting to false + awx.awx.settings: + name: "PROJECT_UPDATE_VVV" + value: false + +- name: Change project vvv setting to true + awx.awx.settings: + name: "PROJECT_UPDATE_VVV" + value: true + register: result + +- name: Changing setting to true should have changed the value + assert: + that: + - "result is changed" + +- name: Change project vvv setting to true + awx.awx.settings: + name: "PROJECT_UPDATE_VVV" + value: true + register: result + +- name: Changing setting to true again should not change the value + assert: + that: + - "result is not changed" + +- name: Change project vvv setting back to false + awx.awx.settings: + name: "PROJECT_UPDATE_VVV" + value: false + register: result + +- name: Changing setting back to false should have changed the value + assert: + that: + - "result is changed" + - name: Set the value of AWX_ISOLATION_SHOW_PATHS to a baseline settings: name: AWX_ISOLATION_SHOW_PATHS diff --git a/ansible_collections/awx/awx/tests/integration/targets/team/tasks/main.yml b/ansible_collections/awx/awx/tests/integration/targets/team/tasks/main.yml index f220d9194..52f02b067 100644 --- a/ansible_collections/awx/awx/tests/integration/targets/team/tasks/main.yml +++ b/ansible_collections/awx/awx/tests/integration/targets/team/tasks/main.yml @@ -1,7 +1,12 @@ --- +- name: Generate a test ID + set_fact: + test_id: "{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}" + when: test_id is not defined + - name: Generate names set_fact: - team_name: "AWX-Collection-tests-team-team-{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}" + team_name: "AWX-Collection-tests-team-team-{{ test_id }}" - name: Attempt to add a team to a non-existant Organization team: @@ -29,6 +34,39 @@ that: - "result is changed" +- name: Create a team with exists + team: + name: "{{ team_name }}" + organization: Default + state: exists + register: result + +- assert: + that: + - "result is not changed" + +- name: Delete a team + team: + name: "{{ team_name }}" + organization: Default + state: absent + register: result + +- assert: + that: + - "result is changed" + +- name: Create a team with exists + team: + name: "{{ team_name }}" + organization: Default + state: exists + register: result + +- assert: + that: + - "result is changed" + - name: Delete a team team: name: "{{ team_name }}" diff --git a/ansible_collections/awx/awx/tests/integration/targets/token/tasks/main.yml b/ansible_collections/awx/awx/tests/integration/targets/token/tasks/main.yml index f13bc6bc6..9cd4972a9 100644 --- a/ansible_collections/awx/awx/tests/integration/targets/token/tasks/main.yml +++ b/ansible_collections/awx/awx/tests/integration/targets/token/tasks/main.yml @@ -1,7 +1,12 @@ --- +- name: Generate a test ID + set_fact: + test_id: "{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}" + when: test_id is not defined + - name: Generate names set_fact: - token_description: "AWX-Collection-tests-token-description-{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}" + token_description: "AWX-Collection-tests-token-description-{{ test_id }}" - name: Try to use a token as a dict which is missing the token parameter job_list: diff --git a/ansible_collections/awx/awx/tests/integration/targets/user/tasks/main.yml b/ansible_collections/awx/awx/tests/integration/targets/user/tasks/main.yml index 1d5cc5de5..a3fae666b 100644 --- a/ansible_collections/awx/awx/tests/integration/targets/user/tasks/main.yml +++ b/ansible_collections/awx/awx/tests/integration/targets/user/tasks/main.yml @@ -1,7 +1,12 @@ --- +- name: Generate a test ID + set_fact: + test_id: "{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}" + when: test_id is not defined + - name: Generate names set_fact: - username: "AWX-Collection-tests-user-user-{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}" + username: "AWX-Collection-tests-user-user-{{ test_id }}" - name: Create a User user: @@ -15,6 +20,42 @@ that: - "result is changed" +- name: Create a User with exists + user: + username: "{{ username }}" + first_name: Joe + password: "{{ 65535 | random | to_uuid }}" + state: exists + register: result + +- assert: + that: + - "result is not changed" + +- name: Delete a User + user: + username: "{{ username }}" + first_name: Joe + password: "{{ 65535 | random | to_uuid }}" + state: absent + register: result + +- assert: + that: + - "result is changed" + +- name: Create a User with exists + user: + username: "{{ username }}" + first_name: Joe + password: "{{ 65535 | random | to_uuid }}" + state: exists + register: result + +- assert: + that: + - "result is changed" + - name: Change a User by ID user: username: "{{ result.id }}" @@ -131,10 +172,6 @@ 'Can not verify ssl with non-https protocol' in result.exception" - block: - - name: Generate a test ID - set_fact: - test_id: "{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}" - - name: Generate an org name set_fact: org_name: "AWX-Collection-tests-organization-org-{{ test_id }}" @@ -183,6 +220,7 @@ user: controller_username: "{{ username }}-orgadmin" controller_password: "{{ username }}-orgadmin" + controller_oauthtoken: false # Hack for CI where we use oauth in config file username: "{{ username }}" first_name: Joe password: "{{ 65535 | random | to_uuid }}" diff --git a/ansible_collections/awx/awx/tests/integration/targets/workflow_approval/tasks/main.yml b/ansible_collections/awx/awx/tests/integration/targets/workflow_approval/tasks/main.yml index eaf1b3bf8..52fb7585c 100644 --- a/ansible_collections/awx/awx/tests/integration/targets/workflow_approval/tasks/main.yml +++ b/ansible_collections/awx/awx/tests/integration/targets/workflow_approval/tasks/main.yml @@ -2,13 +2,15 @@ - name: Generate a random string for names set_fact: test_id: "{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}" - test_prefix: AWX-Collection-tests-workflow_approval + when: test_id is not defined - name: Generate random names for test objects set_fact: org_name: "{{ test_prefix }}-org-{{ test_id }}" approval_node_name: "{{ test_prefix }}-node-{{ test_id }}" wfjt_name: "{{ test_prefix }}-wfjt-{{ test_id }}" + vars: + test_prefix: AWX-Collection-tests-workflow_approval - block: - name: Create a new organization for test isolation diff --git a/ansible_collections/awx/awx/tests/integration/targets/workflow_job_template/tasks/main.yml b/ansible_collections/awx/awx/tests/integration/targets/workflow_job_template/tasks/main.yml index e5f3366cd..f051e47ad 100644 --- a/ansible_collections/awx/awx/tests/integration/targets/workflow_job_template/tasks/main.yml +++ b/ansible_collections/awx/awx/tests/integration/targets/workflow_job_template/tasks/main.yml @@ -2,6 +2,7 @@ - name: Generate a random string for names set_fact: test_id: "{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}" + when: test_id is not defined - name: Generate random names for test objects set_fact: @@ -17,8 +18,8 @@ webhook_wfjt_name: "AWX-Collection-tests-workflow_job_template-webhook-wfjt-{{ test_id }}" email_not: "AWX-Collection-tests-job_template-email-not-{{ test_id }}" webhook_notification: "AWX-Collection-tests-notification_template-wehbook-not-{{ test_id }}" - project_inv: "AWX-Collection-tests-inventory_source-inv-project-{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}" - project_inv_source: "AWX-Collection-tests-inventory_source-inv-source-project-{{ lookup('password', '/dev/null chars=ascii_letters length=16') }}" + project_inv: "AWX-Collection-tests-inventory_source-inv-project-{{ test_id }}" + project_inv_source: "AWX-Collection-tests-inventory_source-inv-source-project-{{ test_id }}" github_webhook_credential_name: "AWX-Collection-tests-credential-webhook-{{ test_id }}_github" ee1: "AWX-Collection-tests-workflow_job_template-ee1-{{ test_id }}" label1: "AWX-Collection-tests-workflow_job_template-l1-{{ test_id }}" @@ -168,6 +169,9 @@ name: "{{ jt1_name }}" project: "{{ demo_project_name }}" inventory: Demo Inventory + ask_inventory_on_launch: true + ask_credential_on_launch: true + ask_labels_on_launch: true playbook: hello_world.yml job_type: run state: present @@ -253,6 +257,65 @@ that: - "result is changed" + - name: Create a workflow job template with exists + workflow_job_template: + name: "{{ wfjt_name }}" + organization: Default + inventory: Demo Inventory + extra_vars: {'foo': 'bar', 'another-foo': {'barz': 'bar2'}} + labels: + - "{{ lab1 }}" + ask_inventory_on_launch: true + ask_scm_branch_on_launch: true + ask_limit_on_launch: true + ask_tags_on_launch: true + ask_variables_on_launch: true + state: exists + register: result + + - assert: + that: + - "result is not changed" + + - name: Delete a workflow job template + workflow_job_template: + name: "{{ wfjt_name }}" + organization: Default + inventory: Demo Inventory + extra_vars: {'foo': 'bar', 'another-foo': {'barz': 'bar2'}} + labels: + - "{{ lab1 }}" + ask_inventory_on_launch: true + ask_scm_branch_on_launch: true + ask_limit_on_launch: true + ask_tags_on_launch: true + ask_variables_on_launch: true + state: absent + register: result + + - assert: + that: + - "result is changed" + + - name: Create a workflow job template with exists + workflow_job_template: + name: "{{ wfjt_name }}" + organization: Default + inventory: Demo Inventory + extra_vars: {'foo': 'bar', 'another-foo': {'barz': 'bar2'}} + # We don't try with the label here because after we delete the first WFJT the label is delete with it because it has no references + ask_inventory_on_launch: true + ask_scm_branch_on_launch: true + ask_limit_on_launch: true + ask_tags_on_launch: true + ask_variables_on_launch: true + state: exists + register: result + + - assert: + that: + - "result is changed" + - name: Create a workflow job template with bad label workflow_job_template: name: "{{ wfjt_name }}" @@ -650,7 +713,7 @@ name: "{{ wfjt_name }}" inventory: Demo Inventory extra_vars: {'foo': 'bar', 'another-foo': {'barz': 'bar2'}} - schema: + workflow_nodes: - identifier: node101 unified_job_template: name: "{{ project_inv_source_result.id }}" @@ -661,30 +724,52 @@ related: failure_nodes: - identifier: node201 + - identifier: node102 + unified_job_template: + organization: + name: "{{ org_name }}" + name: "{{ demo_project_name_2 }}" + type: project + related: + success_nodes: + - identifier: node201 - identifier: node201 unified_job_template: organization: name: Default name: "{{ jt1_name }}" type: job_template - credentials: [] + inventory: + name: Demo Inventory + organization: + name: Default related: success_nodes: + - identifier: node401 + failure_nodes: - identifier: node301 - - identifier: node202 - unified_job_template: - organization: - name: "{{ org_name }}" - name: "{{ project_inv_source }}" - type: project + always_nodes: [] + credentials: + - name: "{{ scm_cred_name }}" + organization: + name: Default + instance_groups: + - name: "{{ ig1 }}" + labels: + - name: "{{ lab1 }}" + organization: + name: "{{ org_name }}" - all_parents_must_converge: false identifier: node301 unified_job_template: - organization: - name: Default - name: "{{ jt2_name }}" - type: job_template - - identifier: Cleanup Job + description: Approval node for example + timeout: 900 + type: workflow_approval + name: "{{ approval_node_name }}" + related: + success_nodes: + - identifier: node401 + - identifier: node401 unified_job_template: name: Cleanup Activity Stream type: system_job_template diff --git a/ansible_collections/awx/awx/tests/integration/targets/workflow_launch/tasks/main.yml b/ansible_collections/awx/awx/tests/integration/targets/workflow_launch/tasks/main.yml index a328b8380..ce41c6bb8 100644 --- a/ansible_collections/awx/awx/tests/integration/targets/workflow_launch/tasks/main.yml +++ b/ansible_collections/awx/awx/tests/integration/targets/workflow_launch/tasks/main.yml @@ -57,7 +57,7 @@ - assert: that: - result is failed - - "'Monitoring of Workflow Job - {{ wfjt_name1 }} aborted due to timeout' in result.msg" + - "'Monitoring of Workflow Job - '~ wfjt_name1 ~ ' aborted due to timeout' in result.msg" - name: Kick off a workflow and wait for it workflow_launch: diff --git a/ansible_collections/awx/awx/tests/sanity/ignore-2.15.txt b/ansible_collections/awx/awx/tests/sanity/ignore-2.15.txt index 19512ea0c..b502cada1 100644 --- a/ansible_collections/awx/awx/tests/sanity/ignore-2.15.txt +++ b/ansible_collections/awx/awx/tests/sanity/ignore-2.15.txt @@ -1 +1,3 @@ plugins/modules/export.py validate-modules:nonexistent-parameter-documented # needs awxkit to construct argspec +plugins/modules/import.py pylint:unused-import # Simply used as a feature flag conditional +test/awx/conftest.py pylint:unused-import # Used to make sure we are importing the right awxkit, see comment in conftest.py near imports diff --git a/ansible_collections/awx/awx/tests/sanity/ignore-2.16.txt b/ansible_collections/awx/awx/tests/sanity/ignore-2.16.txt new file mode 100644 index 000000000..b502cada1 --- /dev/null +++ b/ansible_collections/awx/awx/tests/sanity/ignore-2.16.txt @@ -0,0 +1,3 @@ +plugins/modules/export.py validate-modules:nonexistent-parameter-documented # needs awxkit to construct argspec +plugins/modules/import.py pylint:unused-import # Simply used as a feature flag conditional +test/awx/conftest.py pylint:unused-import # Used to make sure we are importing the right awxkit, see comment in conftest.py near imports |