From 4c60999161a11f4c258e12596e4bfcee78ef5e12 Mon Sep 17 00:00:00 2001 From: Ryan Petrello Date: Fri, 5 Mar 2021 14:11:30 -0500 Subject: [PATCH] remove custom_virtualenv support from the AWX collection and docs --- awx/api/serializers.py | 4 + .../tests/functional/api/test_job_template.py | 59 ------ .../functional/api/test_organizations.py | 31 ---- awx/main/tests/functional/api/test_project.py | 30 ---- .../plugins/modules/tower_inventory_source.py | 7 +- .../plugins/modules/tower_job_template.py | 8 +- .../plugins/modules/tower_organization.py | 10 -- .../plugins/modules/tower_project.py | 9 - .../test/awx/test_inventory_source.py | 55 ------ awx_collection/test/awx/test_organization.py | 22 --- .../targets/tower_import/tasks/main.yml | 3 - .../targets/tower_organization/tasks/main.yml | 13 -- awx_collection/tools/vars/examples.yml | 1 - docs/custom_virtualenvs.md | 168 ------------------ 14 files changed, 6 insertions(+), 414 deletions(-) delete mode 100644 docs/custom_virtualenvs.md diff --git a/awx/api/serializers.py b/awx/api/serializers.py index 6f8dcd3fad..8246ad8f1d 100644 --- a/awx/api/serializers.py +++ b/awx/api/serializers.py @@ -1253,6 +1253,7 @@ class OrganizationSerializer(BaseSerializer): class Meta: model = Organization fields = ('*', 'max_hosts', 'custom_virtualenv', 'default_environment',) + read_only_fields = ('*', 'custom_virtualenv',) def get_related(self, obj): res = super(OrganizationSerializer, self).get_related(obj) @@ -1399,6 +1400,7 @@ class ProjectSerializer(UnifiedJobTemplateSerializer, ProjectOptionsSerializer): fields = ('*', '-execution_environment', 'organization', 'scm_update_on_launch', 'scm_update_cache_timeout', 'allow_override', 'custom_virtualenv', 'default_environment') + \ ('last_update_failed', 'last_updated') # Backwards compatibility + read_only_fields = ('*', 'custom_virtualenv',) def get_related(self, obj): res = super(ProjectSerializer, self).get_related(obj) @@ -1978,6 +1980,7 @@ class InventorySourceOptionsSerializer(BaseSerializer): fields = ('*', 'source', 'source_path', 'source_script', 'source_vars', 'credential', 'enabled_var', 'enabled_value', 'host_filter', 'overwrite', 'overwrite_vars', 'custom_virtualenv', 'timeout', 'verbosity') + read_only_fields = ('*', 'custom_virtualenv',) def get_related(self, obj): res = super(InventorySourceOptionsSerializer, self).get_related(obj) @@ -2963,6 +2966,7 @@ class JobTemplateSerializer(JobTemplateMixin, UnifiedJobTemplateSerializer, JobO 'become_enabled', 'diff_mode', 'allow_simultaneous', 'custom_virtualenv', 'job_slice_count', 'webhook_service', 'webhook_credential', ) + read_only_fields = ('*', 'custom_virtualenv',) def get_related(self, obj): res = super(JobTemplateSerializer, self).get_related(obj) diff --git a/awx/main/tests/functional/api/test_job_template.py b/awx/main/tests/functional/api/test_job_template.py index 1883f6e6ec..2eeefa7eed 100644 --- a/awx/main/tests/functional/api/test_job_template.py +++ b/awx/main/tests/functional/api/test_job_template.py @@ -1,6 +1,3 @@ -import os - -from backports.tempfile import TemporaryDirectory import pytest # AWX @@ -10,7 +7,6 @@ from awx.main.models import Job, JobTemplate, CredentialType, WorkflowJobTemplat from awx.main.migrations import _save_password_keys as save_password_keys # Django -from django.conf import settings from django.apps import apps # DRF @@ -302,61 +298,6 @@ def test_save_survey_passwords_on_migration(job_template_with_survey_passwords): assert job.survey_passwords == {'SSN': '$encrypted$', 'secret_key': '$encrypted$'} -@pytest.mark.django_db -@pytest.mark.parametrize('access', ["superuser", "admin", "peon"]) -def test_job_template_custom_virtualenv(get, patch, organization_factory, job_template_factory, alice, access): - objs = organization_factory("org", superusers=['admin']) - jt = job_template_factory("jt", organization=objs.organization, - inventory='test_inv', project='test_proj').job_template - - user = alice - if access == "superuser": - user = objs.superusers.admin - elif access == "admin": - jt.admin_role.members.add(alice) - else: - jt.read_role.members.add(alice) - - with TemporaryDirectory(dir=settings.BASE_VENV_PATH) as temp_dir: - os.makedirs(os.path.join(temp_dir, 'bin', 'activate')) - url = reverse('api:job_template_detail', kwargs={'pk': jt.id}) - - if access == "peon": - patch(url, {'custom_virtualenv': temp_dir}, user=user, expect=403) - assert 'custom_virtualenv' not in get(url, user=user) - assert JobTemplate.objects.get(pk=jt.id).custom_virtualenv is None - else: - patch(url, {'custom_virtualenv': temp_dir}, user=user, expect=200) - assert get(url, user=user).data['custom_virtualenv'] == os.path.join(temp_dir, '') - - -@pytest.mark.django_db -def test_job_template_invalid_custom_virtualenv(get, patch, organization_factory, - job_template_factory): - objs = organization_factory("org", superusers=['admin']) - jt = job_template_factory("jt", organization=objs.organization, - inventory='test_inv', project='test_proj').job_template - - url = reverse('api:job_template_detail', kwargs={'pk': jt.id}) - resp = patch(url, {'custom_virtualenv': '/foo/bar'}, user=objs.superusers.admin, expect=400) - assert resp.data['custom_virtualenv'] == [ - '/foo/bar is not a valid virtualenv in {}'.format(settings.BASE_VENV_PATH) - ] - - -@pytest.mark.django_db -@pytest.mark.parametrize('value', ["", None]) -def test_job_template_unset_custom_virtualenv(get, patch, organization_factory, - job_template_factory, value): - objs = organization_factory("org", superusers=['admin']) - jt = job_template_factory("jt", organization=objs.organization, - inventory='test_inv', project='test_proj').job_template - - url = reverse('api:job_template_detail', kwargs={'pk': jt.id}) - resp = patch(url, {'custom_virtualenv': value}, user=objs.superusers.admin, expect=200) - assert resp.data['custom_virtualenv'] is None - - @pytest.mark.django_db def test_jt_organization_follows_project(post, patch, admin_user): org1 = Organization.objects.create(name='foo1') diff --git a/awx/main/tests/functional/api/test_organizations.py b/awx/main/tests/functional/api/test_organizations.py index 6c45c0c681..93994d3842 100644 --- a/awx/main/tests/functional/api/test_organizations.py +++ b/awx/main/tests/functional/api/test_organizations.py @@ -1,11 +1,6 @@ # Copyright (c) 2015 Ansible, Inc. # All Rights Reserved. -# Python -import os - -from backports.tempfile import TemporaryDirectory -from django.conf import settings import pytest # AWX @@ -242,32 +237,6 @@ def test_delete_organization_xfail2(delete, organization): delete(reverse('api:organization_detail', kwargs={'pk': organization.id}), user=None, expect=401) -@pytest.mark.django_db -def test_organization_custom_virtualenv(get, patch, organization, admin): - with TemporaryDirectory(dir=settings.BASE_VENV_PATH) as temp_dir: - os.makedirs(os.path.join(temp_dir, 'bin', 'activate')) - url = reverse('api:organization_detail', kwargs={'pk': organization.id}) - patch(url, {'custom_virtualenv': temp_dir}, user=admin, expect=200) - assert get(url, user=admin).data['custom_virtualenv'] == os.path.join(temp_dir, '') - - -@pytest.mark.django_db -def test_organization_invalid_custom_virtualenv(get, patch, organization, admin): - url = reverse('api:organization_detail', kwargs={'pk': organization.id}) - resp = patch(url, {'custom_virtualenv': '/foo/bar'}, user=admin, expect=400) - assert resp.data['custom_virtualenv'] == [ - '/foo/bar is not a valid virtualenv in {}'.format(settings.BASE_VENV_PATH) - ] - - -@pytest.mark.django_db -@pytest.mark.parametrize('value', ["", None]) -def test_organization_unset_custom_virtualenv(get, patch, organization, admin, value): - url = reverse('api:organization_detail', kwargs={'pk': organization.id}) - resp = patch(url, {'custom_virtualenv': value}, user=admin, expect=200) - assert resp.data['custom_virtualenv'] is None - - @pytest.mark.django_db def test_organization_delete(delete, admin, organization, organization_jobs_successful): url = reverse('api:organization_detail', kwargs={'pk': organization.id}) diff --git a/awx/main/tests/functional/api/test_project.py b/awx/main/tests/functional/api/test_project.py index a31eb0804a..71d685ce7b 100644 --- a/awx/main/tests/functional/api/test_project.py +++ b/awx/main/tests/functional/api/test_project.py @@ -1,7 +1,3 @@ -import os - -from backports.tempfile import TemporaryDirectory -from django.conf import settings import pytest from awx.api.versioning import reverse @@ -21,32 +17,6 @@ class TestInsightsCredential: expect=400) -@pytest.mark.django_db -def test_project_custom_virtualenv(get, patch, project, admin): - with TemporaryDirectory(dir=settings.BASE_VENV_PATH) as temp_dir: - os.makedirs(os.path.join(temp_dir, 'bin', 'activate')) - url = reverse('api:project_detail', kwargs={'pk': project.id}) - patch(url, {'custom_virtualenv': temp_dir}, user=admin, expect=200) - assert get(url, user=admin).data['custom_virtualenv'] == os.path.join(temp_dir, '') - - -@pytest.mark.django_db -def test_project_invalid_custom_virtualenv(get, patch, project, admin): - url = reverse('api:project_detail', kwargs={'pk': project.id}) - resp = patch(url, {'custom_virtualenv': '/foo/bar'}, user=admin, expect=400) - assert resp.data['custom_virtualenv'] == [ - '/foo/bar is not a valid virtualenv in {}'.format(settings.BASE_VENV_PATH) - ] - - -@pytest.mark.django_db -@pytest.mark.parametrize('value', ["", None]) -def test_project_unset_custom_virtualenv(get, patch, project, admin, value): - url = reverse('api:project_detail', kwargs={'pk': project.id}) - resp = patch(url, {'custom_virtualenv': value}, user=admin, expect=200) - assert resp.data['custom_virtualenv'] is None - - @pytest.mark.django_db def test_no_changing_overwrite_behavior_if_used(post, patch, organization, admin_user): r1 = post( diff --git a/awx_collection/plugins/modules/tower_inventory_source.py b/awx_collection/plugins/modules/tower_inventory_source.py index 9edf467617..0bde4d7af3 100644 --- a/awx_collection/plugins/modules/tower_inventory_source.py +++ b/awx_collection/plugins/modules/tower_inventory_source.py @@ -85,10 +85,6 @@ options: description: - Override vars in child groups and hosts with those from external source. type: bool - custom_virtualenv: - description: - - Local absolute file path containing a custom Python virtualenv to use. - type: str timeout: description: The amount of time (in seconds) to run before the task is canceled. type: int @@ -181,7 +177,6 @@ def main(): organization=dict(), overwrite=dict(type='bool'), overwrite_vars=dict(type='bool'), - custom_virtualenv=dict(), timeout=dict(type='int'), verbosity=dict(type='int', choices=[0, 1, 2]), update_on_launch=dict(type='bool'), @@ -265,7 +260,7 @@ def main(): OPTIONAL_VARS = ( 'description', 'source', 'source_path', 'source_vars', - 'overwrite', 'overwrite_vars', 'custom_virtualenv', + 'overwrite', 'overwrite_vars', 'timeout', 'verbosity', 'update_on_launch', 'update_cache_timeout', 'update_on_project_update', 'enabled_var', 'enabled_value', 'host_filter', ) diff --git a/awx_collection/plugins/modules/tower_job_template.py b/awx_collection/plugins/modules/tower_job_template.py index 1ed750b86e..bb52b4d4ee 100644 --- a/awx_collection/plugins/modules/tower_job_template.py +++ b/awx_collection/plugins/modules/tower_job_template.py @@ -227,10 +227,6 @@ options: description: - Maximum time in seconds to wait for a job to finish (server-side). type: int - custom_virtualenv: - description: - - Local absolute file path containing a custom Python virtualenv to use. - type: str 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. @@ -304,7 +300,6 @@ EXAMPLES = ''' tower_config_file: "~/tower_cli.cfg" survey_enabled: yes survey_spec: "{{ lookup('file', 'my_survey.json') }}" - custom_virtualenv: "/var/lib/awx/venv/custom-venv/" - name: Add start notification to Job Template tower_job_template: @@ -352,7 +347,6 @@ def main(): playbook=dict(), credential=dict(), vault_credential=dict(), - custom_virtualenv=dict(), credentials=dict(type='list', elements='str'), execution_environment=dict(), forks=dict(type='int'), @@ -442,7 +436,7 @@ def main(): 'host_config_key', 'ask_scm_branch_on_launch', 'ask_diff_mode_on_launch', 'ask_variables_on_launch', 'ask_limit_on_launch', 'ask_tags_on_launch', 'ask_skip_tags_on_launch', 'ask_job_type_on_launch', 'ask_verbosity_on_launch', 'ask_inventory_on_launch', 'ask_credential_on_launch', 'survey_enabled', - 'become_enabled', 'diff_mode', 'allow_simultaneous', 'custom_virtualenv', 'job_slice_count', 'webhook_service', + 'become_enabled', 'diff_mode', 'allow_simultaneous', 'job_slice_count', 'webhook_service', ): field_val = module.params.get(field_name) if field_val is not None: diff --git a/awx_collection/plugins/modules/tower_organization.py b/awx_collection/plugins/modules/tower_organization.py index bcf6060ea6..197099d391 100644 --- a/awx_collection/plugins/modules/tower_organization.py +++ b/awx_collection/plugins/modules/tower_organization.py @@ -31,11 +31,6 @@ options: description: - The description to use for the organization. type: str - custom_virtualenv: - description: - - Local absolute file path containing a custom Python virtualenv to use. - type: str - default: '' default_environment: description: - Default Execution Environment to use for jobs owned by the Organization. @@ -92,7 +87,6 @@ EXAMPLES = ''' tower_organization: name: "Foo" description: "Foo bar organization using foo-venv" - custom_virtualenv: "/var/lib/awx/venv/foo-venv/" state: present tower_config_file: "~/tower_cli.cfg" @@ -113,7 +107,6 @@ def main(): argument_spec = dict( name=dict(required=True), description=dict(), - custom_virtualenv=dict(), default_environment=dict(), max_hosts=dict(type='int', default="0"), notification_templates_started=dict(type="list", elements='str'), @@ -130,7 +123,6 @@ def main(): # Extract our parameters name = module.params.get('name') description = module.params.get('description') - custom_virtualenv = module.params.get('custom_virtualenv') default_ee = module.params.get('default_environment') max_hosts = module.params.get('max_hosts') # instance_group_names = module.params.get('instance_groups') @@ -179,8 +171,6 @@ def main(): org_fields = {'name': module.get_item_name(organization) if organization else name} if description is not None: org_fields['description'] = description - if custom_virtualenv is not None: - org_fields['custom_virtualenv'] = custom_virtualenv if default_ee is not None: org_fields['default_environment'] = module.resolve_name_to_id('execution_environments', default_ee) if max_hosts is not None: diff --git a/awx_collection/plugins/modules/tower_project.py b/awx_collection/plugins/modules/tower_project.py index acbf3833b5..42264996cb 100644 --- a/awx_collection/plugins/modules/tower_project.py +++ b/awx_collection/plugins/modules/tower_project.py @@ -97,11 +97,6 @@ options: type: int aliases: - job_timeout - custom_virtualenv: - description: - - Local absolute file path containing a custom Python virtualenv to use - type: str - default: '' default_environment: description: - Default Execution Environment to use for jobs relating to the project. @@ -172,7 +167,6 @@ EXAMPLES = ''' organization: "test" scm_update_on_launch: True scm_update_cache_timeout: 60 - custom_virtualenv: "/var/lib/awx/var/lib/awx/venv/ansible-2.2" state: present tower_config_file: "~/tower_cli.cfg" ''' @@ -242,7 +236,6 @@ def main(): scm_update_cache_timeout=dict(type='int', default=0), allow_override=dict(type='bool', aliases=['scm_allow_override']), timeout=dict(type='int', default=0, aliases=['job_timeout']), - custom_virtualenv=dict(), default_environment=dict(), organization=dict(), notification_templates_started=dict(type="list", elements='str'), @@ -274,7 +267,6 @@ def main(): scm_update_cache_timeout = module.params.get('scm_update_cache_timeout') allow_override = module.params.get('allow_override') timeout = module.params.get('timeout') - custom_virtualenv = module.params.get('custom_virtualenv') default_ee = module.params.get('default_environment') organization = module.params.get('organization') state = module.params.get('state') @@ -333,7 +325,6 @@ def main(): 'organization': org_id, 'scm_update_on_launch': scm_update_on_launch, 'scm_update_cache_timeout': scm_update_cache_timeout, - 'custom_virtualenv': custom_virtualenv, } if description is not None: project_fields['description'] = description diff --git a/awx_collection/test/awx/test_inventory_source.py b/awx_collection/test/awx/test_inventory_source.py index fa01b16ddb..fabd3d9fb8 100644 --- a/awx_collection/test/awx/test_inventory_source.py +++ b/awx_collection/test/awx/test_inventory_source.py @@ -97,61 +97,6 @@ def test_create_inventory_source_multiple_orgs(run_module, admin_user): } -@pytest.mark.django_db -def test_create_inventory_source_with_venv(run_module, admin_user, base_inventory, mocker, project): - path = '/var/lib/awx/venv/custom-venv/foobar13489435/' - source_path = '/var/lib/awx/example_source_path/' - with mocker.patch('awx.main.models.mixins.get_custom_venv_choices', return_value=[path]): - result = run_module('tower_inventory_source', dict( - name='foo', - inventory=base_inventory.name, - state='present', - source='scm', - source_project=project.name, - custom_virtualenv=path, - source_path=source_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, project): - """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. - """ - source_path = '/var/lib/awx/example_source_path/' - inv_src = InventorySource.objects.create( - name='foo', - inventory=base_inventory, - source_project=project, - source='scm', - custom_virtualenv='/var/lib/awx/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=['/var/lib/awx/venv/foobar/']): - result = run_module('tower_inventory_source', dict( - name='foo', - description='this is the changed description', - inventory=base_inventory.name, - source='scm', # is required, but behavior is arguable - state='present', - source_project=project.name, - source_path=source_path - ), admin_user) - assert result.pop('changed', None), result - inv_src.refresh_from_db() - assert inv_src.custom_virtualenv == '/var/lib/awx/venv/foobar/' - assert inv_src.description == 'this is the changed description' - - @pytest.mark.django_db def test_falsy_value(run_module, admin_user, base_inventory): result = run_module('tower_inventory_source', dict( diff --git a/awx_collection/test/awx/test_organization.py b/awx_collection/test/awx/test_organization.py index 8f4872c303..ee58ab3a2c 100644 --- a/awx_collection/test/awx/test_organization.py +++ b/awx_collection/test/awx/test_organization.py @@ -20,7 +20,6 @@ def test_create_organization(run_module, admin_user): 'validate_certs': None, 'tower_oauthtoken': None, 'tower_config_file': None, - 'custom_virtualenv': None } result = run_module('tower_organization', module_args, admin_user) @@ -37,24 +36,3 @@ 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 == { - "name": "foo", - "id": org.id - } - - assert org.custom_virtualenv == path diff --git a/awx_collection/tests/integration/targets/tower_import/tasks/main.yml b/awx_collection/tests/integration/targets/tower_import/tasks/main.yml index dbfaf5c06a..5c04958ac7 100644 --- a/awx_collection/tests/integration/targets/tower_import/tasks/main.yml +++ b/awx_collection/tests/integration/targets/tower_import/tasks/main.yml @@ -17,7 +17,6 @@ - name: "{{ org_name1 }}" description: "" max_hosts: 0 - custom_virtualenv: null related: notification_templates: [] notification_templates_started: [] @@ -40,7 +39,6 @@ - name: "{{ org_name1 }}" description: "" max_hosts: 0 - custom_virtualenv: null related: notification_templates: [] notification_templates_started: [] @@ -67,7 +65,6 @@ "name": "{{ org_name2 }}", "description": "", "max_hosts": 0, - "custom_virtualenv": null, "related": { "notification_templates": [], "notification_templates_started": [], diff --git a/awx_collection/tests/integration/targets/tower_organization/tasks/main.yml b/awx_collection/tests/integration/targets/tower_organization/tasks/main.yml index d6b174f674..a1523c45e6 100644 --- a/awx_collection/tests/integration/targets/tower_organization/tasks/main.yml +++ b/awx_collection/tests/integration/targets/tower_organization/tasks/main.yml @@ -28,22 +28,10 @@ that: - "result is not changed" -- name: "Try adding a bad custom_virtualenv" - tower_organization: - name: "{{ org_name }}" - custom_virtualenv: "/does/not/exit" - register: result - ignore_errors: true - -- assert: - that: - - "result is failed" - - name: "Pass in all parameters" tower_organization: name: "{{ org_name }}" description: "A description" - custom_virtualenv: "" register: result - assert: @@ -54,7 +42,6 @@ tower_organization: name: "{{ org_name }}" description: "A new description" - custom_virtualenv: "" register: result - assert: diff --git a/awx_collection/tools/vars/examples.yml b/awx_collection/tools/vars/examples.yml index 8a8d132881..a927f970b7 100644 --- a/awx_collection/tools/vars/examples.yml +++ b/awx_collection/tools/vars/examples.yml @@ -49,4 +49,3 @@ examples: tower_config_file: "~/tower_cli.cfg" survey_enabled: yes survey_spec: "{{ '{{' }} lookup('file', 'my_survey.json') {{ '}}' }}" - custom_virtualenv: "/var/lib/awx/venv/custom-venv/" diff --git a/docs/custom_virtualenvs.md b/docs/custom_virtualenvs.md deleted file mode 100644 index 1f5b6e6a2d..0000000000 --- a/docs/custom_virtualenvs.md +++ /dev/null @@ -1,168 +0,0 @@ -Managing Custom Python Dependencies -=================================== -awx installations pre-build a special [Python -virtualenv](https://pypi.python.org/pypi/virtualenv) which is automatically -activated for all `ansible-playbook` runs invoked by awx (for example, any time -a Job Template is launched). By default, this virtualenv is located at -`/var/lib/awx/venv/ansible` on the file system. - -awx pre-installs a variety of third-party library/SDK support into this -virtualenv for its integration points with a variety of cloud providers (such -as EC2, OpenStack, Azure, etc...) - -Periodically, awx users want to add additional SDK support into this -virtualenv; this documentation describes the supported way to do so. - -Preparing a New Custom Virtualenv -================================= -awx allows a _different_ virtualenv to be specified and used on Job Template -runs. To choose a custom virtualenv, first we need to create one. Here, we are -using `/opt/my-envs/` as the directory to hold custom venvs. But you can use any -other directory and replace `/opt/my-envs/` with that. Let's create the directory -first if absent: - - NOTE: For docker installations, this directory needs to exist on awx_web AND - awx_task container - - $ sudo mkdir /opt/my-envs - -Now, we need to tell Tower to look into this directory for custom venvs. For that, -we can add this directory to the `CUSTOM_VENV_PATHS` setting as: - - $ HTTP PATCH /api/v2/settings/system/ {'CUSTOM_VENV_PATHS': ["/opt/my-envs/"]} - -If we have venvs spanned over multiple directories, we can add all the paths and -Tower will aggregate venvs from them: - - $ HTTP PATCH /api/v2/settings/system/ {'CUSTOM_VENV_PATHS': ["/path/1/to/venv/", - "/path/2/to/venv/", - "/path/3/to/venv/"]} - -Now that we have the directory setup, we can create a virtual environment in that using: - - $ sudo virtualenv /opt/my-envs/custom-venv - -Multiple versions of Python are supported, though it's important to note that -the semantics for creating virtualenvs in Python 3 has changed slightly: - - $ sudo python3 -m venv /opt/my-envs/custom-venv - -Your newly created virtualenv needs a few base dependencies to properly run -playbooks: -fact gathering): - - $ sudo /opt/my-envs/custom-venv/bin/pip install psutil - -From here, you can install _additional_ Python dependencies that you care -about, such as a per-virtualenv version of ansible itself: - - $ sudo /opt/my-envs/custom-venv/bin/pip install -U "ansible == X.Y.Z" - -...or an additional third-party SDK that's not included with the base awx installation: - - $ sudo /opt/my-envs/custom-venv/bin/pip install -U python-digitalocean - -If you want to copy them, the libraries included in awx's default virtualenv -can be found using `pip freeze`: - - $ sudo /var/lib/awx/venv/ansible/bin/pip freeze - -One important item to keep in mind is that in a clustered awx installation, -you need to ensure that the same custom virtualenv exists on _every_ local file -system at `/opt/my-envs/`. For container-based deployments, this likely -means building these steps into your own custom image building workflow, e.g., - -```diff -diff --git a/Makefile b/Makefile -index aa8b304..eb05f91 100644 ---- a/Makefile -+++ b/Makefile -@@ -164,6 +164,10 @@ requirements_ansible_dev: - $(VENV_BASE)/ansible/bin/pip install pytest mock; \ - fi - -+requirements_custom: -+ mkdir -p /opt/my-envs -+ virtualenv /opt/my-envs/my-custom-env -+ /opt/my-envs/my-custom-env/bin/pip install psutil -+ -diff --git a/installer/roles/image_build/templates/Dockerfile.j2 b/installer/roles/image_build/templates/Dockerfile.j2 -index d3b582ffcb..220ac760a3 100644 ---- a/installer/roles/image_build/templates/Dockerfile.j2 -+++ b/installer/roles/image_build/templates/Dockerfile.j2 -@@ -165,6 +165,7 @@ RUN openssl req -nodes -newkey rsa:2048 -keyout /etc/nginx/nginx.key -out /etc/n - chmod 640 /etc/nginx/nginx.{csr,key,crt} - {% else %} - COPY --from=builder /var/lib/awx /var/lib/awx -+COPY --from=builder /opt/my-envs /opt/my-envs - RUN ln -s /var/lib/awx/venv/awx/bin/awx-manage /usr/bin/awx-manage - {% endif %} -``` - -Once the AWX API is available, update the `CUSTOM_VENV_PATHS` setting as described in `Preparing a New Custom Virtualenv`. - -Kubernetes Custom Virtualenvs -============================= -You can add custom virtual environments without modifying images by including -the following variables in your `install.yml` playbook run. Your variables file -must have a variable called `custom_venvs` with a list of your custom -virtualenvs containing the name, python interpreter, ansible version, and a list -of modules that should be installed in each one: - -```yaml -# venv_vars.yaml ---- -custom_venvs: - - name: dns_team - python: python3 # Defaults to python2 - python_ansible_version: 2.8.1 - python_modules: - - dnspython - - infoblox-client - - name: windows_team - python: python2 - python_ansible_version: 2.8.0 - python_modules: - - winrm - - name: vmware_team - python_ansible_version: 2.7.10 - python_modules: - - pyvmomi -``` - -The virtualenvs will be created in `/opt/custom-venvs` by default, but you can -override that location by setting the variable `custom_venvs_path`. - -You can use the variables file like so: - - $ ansible-playbook -i inventory install.yml --extra-vars "@venv_vars.yaml" - -Once the AWX API is available, you will need to update the `CUSTOM_VENV_PATHS` -setting as described in `Preparing a New Custom Virtualenv`. - -Assigning Custom Virtualenvs -============================ -Once you've created a custom virtualenv, you can assign it at the Organization, -Project, or Job Template level: - -```http -PATCH https://awx-host.example.org/api/v2/organizations/N/ -PATCH https://awx-host.example.org/api/v2/projects/N/ -PATCH https://awx-host.example.org/api/v2/job_templates/N/ - -Content-Type: application/json -{ - "custom_virtualenv": "/opt/my-envs/custom-venv/" -} -``` - -An HTTP `GET` request to `/api/v2/config/` will provide a list of -detected installed virtualenvs: - - { - "custom_virtualenvs": [ - "/opt/my-envs/custom-venv/", - "/opt/my-envs/my-other-custom-venv/", - ], - ... - }