diff --git a/awx_collection/plugins/modules/tower_inventory_source.py b/awx_collection/plugins/modules/tower_inventory_source.py index d71fa96ae5..23823d4337 100644 --- a/awx_collection/plugins/modules/tower_inventory_source.py +++ b/awx_collection/plugins/modules/tower_inventory_source.py @@ -72,6 +72,12 @@ options: change the output of the openstack.py script. It has to be YAML or JSON. type: str + custom_virtualenv: + version_added: "2.9" + description: + - Local absolute file path containing a custom Python virtualenv to use. + type: str + required: False timeout: description: - Number in seconds after which the Tower API methods will time out. @@ -227,6 +233,7 @@ def main(): source_script=dict(required=False), overwrite=dict(type='bool', required=False), overwrite_vars=dict(type='bool', required=False), + custom_virtualenv=dict(type='str', required=False), update_on_launch=dict(type='bool', required=False), update_cache_timeout=dict(type='int', required=False), organization=dict(type='str'), @@ -318,7 +325,7 @@ def main(): changed=False ) - for key in ('source_vars', 'timeout', 'source_path', + for key in ('source_vars', 'custom_virtualenv', 'timeout', 'source_path', 'update_on_project_update', 'source_regions', 'instance_filters', 'group_by', 'overwrite', 'overwrite_vars', 'update_on_launch', diff --git a/awx_collection/plugins/modules/tower_job_template.py b/awx_collection/plugins/modules/tower_job_template.py index 7cfd575000..1a3575eba8 100644 --- a/awx_collection/plugins/modules/tower_job_template.py +++ b/awx_collection/plugins/modules/tower_job_template.py @@ -306,7 +306,7 @@ def main(): playbook=dict(required=True), credential=dict(default=''), vault_credential=dict(default=''), - custom_virtualenv=dict(type='str', required=False, default=''), + custom_virtualenv=dict(type='str', required=False), forks=dict(type='int'), limit=dict(default=''), verbosity=dict(type='int', choices=[0, 1, 2, 3, 4], default=0), diff --git a/awx_collection/plugins/modules/tower_organization.py b/awx_collection/plugins/modules/tower_organization.py index d9573f779f..201c7fc326 100644 --- a/awx_collection/plugins/modules/tower_organization.py +++ b/awx_collection/plugins/modules/tower_organization.py @@ -81,7 +81,7 @@ def main(): argument_spec = dict( name=dict(required=True), description=dict(), - custom_virtualenv=dict(type='str', required=False, default=''), + custom_virtualenv=dict(type='str', required=False), state=dict(choices=['present', 'absent'], default='present'), ) diff --git a/awx_collection/plugins/modules/tower_project.py b/awx_collection/plugins/modules/tower_project.py index 2848e68138..8793dbea72 100644 --- a/awx_collection/plugins/modules/tower_project.py +++ b/awx_collection/plugins/modules/tower_project.py @@ -158,7 +158,7 @@ def main(): scm_update_on_launch=dict(type='bool', default=False), scm_update_cache_timeout=dict(type='int'), job_timeout=dict(type='int', default=0), - custom_virtualenv=dict(type='str', required=False, default=''), + custom_virtualenv=dict(type='str', required=False), local_path=dict(), state=dict(choices=['present', 'absent'], default='present'), wait=dict(type='bool', default=True), diff --git a/awx_collection/test/awx/test_inventory_source.py b/awx_collection/test/awx/test_inventory_source.py index 4953a9a8bc..882156a42f 100644 --- a/awx_collection/test/awx/test_inventory_source.py +++ b/awx_collection/test/awx/test_inventory_source.py @@ -1,6 +1,40 @@ import pytest -from awx.main.models import Organization, Inventory, InventorySource +from awx.main.models import Organization, Inventory, InventorySource, Project + + +@pytest.fixture +def base_inventory(): + org = Organization.objects.create(name='test-org') + inv = Inventory.objects.create(name='test-inv', organization=org) + Project.objects.create( + name='test-proj', + organization=org, + scm_type='git', + scm_url='https://github.com/ansible/test-playbooks.git', + ) + return inv + + +@pytest.mark.django_db +def test_inventory_source_create(run_module, admin_user, base_inventory): + result = run_module('tower_inventory_source', dict( + name='foo', + inventory='test-inv', + state='present', + source='scm', + source_project='test-proj' + ), admin_user) + assert result.pop('changed', None), result + + inv_src = InventorySource.objects.get(name='foo') + assert inv_src.inventory == base_inventory + result.pop('invocation') + assert result == { + 'id': inv_src.id, + 'inventory_source': 'foo', + 'state': 'present' + } @pytest.mark.django_db @@ -54,3 +88,53 @@ def test_create_inventory_source_multiple_orgs(run_module, admin_user): "state": "present", "id": inv_src.id, } + + +@pytest.mark.django_db +def test_create_inventory_source_with_venv(run_module, admin_user, base_inventory, mocker): + path = '/var/lib/awx/venv/custom-venv/foobar13489435/' + with mocker.patch('awx.main.models.mixins.get_custom_venv_choices', return_value=[path]): + result = run_module('tower_inventory_source', dict( + name='foo', + inventory='test-inv', + state='present', + source='scm', + source_project='test-proj', + custom_virtualenv=path + ), admin_user) + assert result.pop('changed'), result + + inv_src = InventorySource.objects.get(name='foo') + assert inv_src.inventory == base_inventory + result.pop('invocation') + + assert inv_src.custom_virtualenv == path + + +@pytest.mark.django_db +def test_custom_venv_no_op(run_module, admin_user, base_inventory, mocker): + """If the inventory source is modified, then it should not blank fields + unrelated to the params that the user passed. + This enforces assumptions about the behavior of the AnsibleModule + default argument_spec behavior. + """ + inv_src = InventorySource.objects.create( + name='foo', + inventory=base_inventory, + source_project=Project.objects.get(name='test-proj'), + source='scm', + custom_virtualenv='/venv/foobar/' + ) + # mock needed due to API behavior, not incorrect client behavior + with mocker.patch('awx.main.models.mixins.get_custom_venv_choices', return_value=['/venv/foobar/']): + result = run_module('tower_inventory_source', dict( + name='foo', + description='this is the changed description', + inventory='test-inv', + source='scm', # is required, but behavior is arguable + state='present' + ), admin_user) + assert result.pop('changed', None), result + inv_src.refresh_from_db() + assert inv_src.custom_virtualenv == '/venv/foobar/' + assert inv_src.description == 'this is the changed description' diff --git a/awx_collection/test/awx/test_organization.py b/awx_collection/test/awx/test_organization.py index ab5f453ae8..1583e7fda5 100644 --- a/awx_collection/test/awx/test_organization.py +++ b/awx_collection/test/awx/test_organization.py @@ -12,6 +12,7 @@ def test_create_organization(run_module, admin_user): module_args = {'name': 'foo', 'description': 'barfoo', 'state': 'present'} result = run_module('tower_organization', module_args, admin_user) + assert result.get('changed'), result org = Organization.objects.get(name='foo') @@ -26,3 +27,26 @@ def test_create_organization(run_module, admin_user): } assert org.description == 'barfoo' + + +@pytest.mark.django_db +def test_create_organization_with_venv(run_module, admin_user, mocker): + path = '/var/lib/awx/venv/custom-venv/foobar13489435/' + with mocker.patch('awx.main.models.mixins.get_custom_venv_choices', return_value=[path]): + result = run_module('tower_organization', { + 'name': 'foo', + 'custom_virtualenv': path, + 'state': 'present' + }, admin_user) + assert result.pop('changed'), result + + org = Organization.objects.get(name='foo') + result.pop('invocation') + + assert result == { + "organization": "foo", + "state": "present", + "id": org.id + } + + assert org.custom_virtualenv == path