diff --git a/awx/api/views/webhooks.py b/awx/api/views/webhooks.py index 207768ed73..39d9dd5ffa 100644 --- a/awx/api/views/webhooks.py +++ b/awx/api/views/webhooks.py @@ -17,7 +17,7 @@ from awx.api import serializers from awx.api.generics import APIView, GenericAPIView from awx.api.permissions import WebhookKeyPermission from awx.main.models import Job, JobTemplate, WorkflowJob, WorkflowJobTemplate -from awx.main.constants import JOB_VARIABLE_PREFIXES +from awx.main.utils.common import get_job_variable_prefixes logger = logging.getLogger('awx.api.views.webhooks') @@ -166,7 +166,7 @@ class WebhookReceiverBase(APIView): 'extra_vars': {}, } - for name in JOB_VARIABLE_PREFIXES: + for name in get_job_variable_prefixes(): kwargs['extra_vars']['{}_webhook_event_type'.format(name)] = event_type kwargs['extra_vars']['{}_webhook_event_guid'.format(name)] = event_guid kwargs['extra_vars']['{}_webhook_event_ref'.format(name)] = event_ref diff --git a/awx/main/conf.py b/awx/main/conf.py index 0ef98cc854..39bd206b09 100644 --- a/awx/main/conf.py +++ b/awx/main/conf.py @@ -325,6 +325,22 @@ register( category_slug='jobs', ) +register( + 'INCLUDE_DEPRECATED_AWX_VAR_PREFIX', + field_class=fields.BooleanField, + default=True, + label=_('Include Deprecated AWX Variable Prefix'), + help_text=_( + 'When enabled (default), auto-generated job variables are emitted ' + 'with both the tower_ prefix and the deprecated awx_ prefix for ' + 'backward compatibility. Disable to emit only tower_ prefixed ' + 'variables and eliminate duplicates. The awx_ prefix is deprecated ' + 'and this setting will default to False in a future release.' + ), + category=_('Jobs'), + category_slug='jobs', +) + register( 'AWX_ISOLATION_BASE_PATH', field_class=fields.CharField, diff --git a/awx/main/constants.py b/awx/main/constants.py index 44262a1838..5dd3f890fd 100644 --- a/awx/main/constants.py +++ b/awx/main/constants.py @@ -100,10 +100,6 @@ MAX_ISOLATED_PATH_COLON_DELIMITER = 2 SURVEY_TYPE_MAPPING = {'text': str, 'textarea': str, 'password': str, 'multiplechoice': str, 'multiselect': str, 'integer': int, 'float': (float, int)} -JOB_VARIABLE_PREFIXES = [ - 'awx', - 'tower', -] # Note, the \u001b[... are ansi color codes. We don't currenly import any of the python modules which define the codes. # Importing a library just for this message seemed like overkill diff --git a/awx/main/models/jobs.py b/awx/main/models/jobs.py index c6e18fff10..8ee50bc05f 100644 --- a/awx/main/models/jobs.py +++ b/awx/main/models/jobs.py @@ -52,7 +52,7 @@ from awx.main.models.mixins import ( WebhookTemplateMixin, OpaQueryPathMixin, ) -from awx.main.constants import JOB_VARIABLE_PREFIXES +from awx.main.utils.common import get_job_variable_prefixes logger = logging.getLogger('awx.main.models.jobs') @@ -817,19 +817,20 @@ class Job(UnifiedJob, JobOptions, SurveyJobMixin, JobNotificationMixin, TaskMana def awx_meta_vars(self): r = super(Job, self).awx_meta_vars() + prefixes = get_job_variable_prefixes() if self.project: - for name in JOB_VARIABLE_PREFIXES: + for name in prefixes: r['{}_project_revision'.format(name)] = self.project.scm_revision r['{}_project_scm_branch'.format(name)] = self.project.scm_branch if self.scm_branch: - for name in JOB_VARIABLE_PREFIXES: + for name in prefixes: r['{}_job_scm_branch'.format(name)] = self.scm_branch if self.job_template: - for name in JOB_VARIABLE_PREFIXES: + for name in prefixes: r['{}_job_template_id'.format(name)] = self.job_template.pk r['{}_job_template_name'.format(name)] = self.job_template.name if self.execution_node: - for name in JOB_VARIABLE_PREFIXES: + for name in prefixes: r['{}_execution_node'.format(name)] = self.execution_node return r diff --git a/awx/main/models/unified_jobs.py b/awx/main/models/unified_jobs.py index f8682451a0..e2de4f45ab 100644 --- a/awx/main/models/unified_jobs.py +++ b/awx/main/models/unified_jobs.py @@ -58,7 +58,8 @@ from awx.main.utils.common import ( ) from awx.main.utils.encryption import encrypt_dict, decrypt_field from awx.main.utils import polymorphic -from awx.main.constants import ACTIVE_STATES, CAN_CANCEL, JOB_VARIABLE_PREFIXES +from awx.main.constants import ACTIVE_STATES, CAN_CANCEL +from awx.main.utils.common import get_job_variable_prefixes from awx.main.redact import UriCleaner, REPLACE_STR from awx.main.consumers import emit_channel_notification from awx.main.fields import AskForField, OrderedManyToManyField @@ -1568,7 +1569,8 @@ class UnifiedJob( by AWX, for purposes of client playbook hooks """ r = {} - for name in JOB_VARIABLE_PREFIXES: + prefixes = get_job_variable_prefixes() + for name in prefixes: r['{}_job_id'.format(name)] = self.pk r['{}_job_launch_type'.format(name)] = self.launch_type @@ -1577,7 +1579,7 @@ class UnifiedJob( wj = self.get_workflow_job() if wj: schedule = getattr_dne(wj, 'schedule') - for name in JOB_VARIABLE_PREFIXES: + for name in prefixes: r['{}_workflow_job_id'.format(name)] = wj.pk r['{}_workflow_job_name'.format(name)] = wj.name r['{}_workflow_job_launch_type'.format(name)] = wj.launch_type @@ -1588,12 +1590,12 @@ class UnifiedJob( if not created_by: schedule = getattr_dne(self, 'schedule') if schedule: - for name in JOB_VARIABLE_PREFIXES: + for name in prefixes: r['{}_schedule_id'.format(name)] = schedule.pk r['{}_schedule_name'.format(name)] = schedule.name if created_by: - for name in JOB_VARIABLE_PREFIXES: + for name in prefixes: r['{}_user_id'.format(name)] = created_by.pk r['{}_user_name'.format(name)] = created_by.username r['{}_user_email'.format(name)] = created_by.email @@ -1602,7 +1604,7 @@ class UnifiedJob( inventory = getattr_dne(self, 'inventory') if inventory: - for name in JOB_VARIABLE_PREFIXES: + for name in prefixes: r['{}_inventory_id'.format(name)] = inventory.pk r['{}_inventory_name'.format(name)] = inventory.name diff --git a/awx/main/tests/functional/models/test_unified_job.py b/awx/main/tests/functional/models/test_unified_job.py index 0618085cef..386d26cf92 100644 --- a/awx/main/tests/functional/models/test_unified_job.py +++ b/awx/main/tests/functional/models/test_unified_job.py @@ -8,7 +8,7 @@ from crum import impersonate # AWX from awx.main.models import UnifiedJobTemplate, Job, JobTemplate, WorkflowJobTemplate, Project, WorkflowJob, Schedule, Credential from awx.api.versioning import reverse -from awx.main.constants import JOB_VARIABLE_PREFIXES +from awx.main.utils.common import get_job_variable_prefixes @pytest.mark.django_db @@ -160,7 +160,13 @@ class TestMetaVars: job = Job.objects.create(name='job', created_by=admin_user) job.save() - user_vars = ['_'.join(x) for x in itertools.product(['tower', 'awx'], ['user_name', 'user_id', 'user_email', 'user_first_name', 'user_last_name'])] + user_vars = [ + '_'.join(x) + for x in itertools.product( + get_job_variable_prefixes(), + ['user_name', 'user_id', 'user_email', 'user_first_name', 'user_last_name'], + ) + ] for key in user_vars: assert key in job.awx_meta_vars() @@ -179,7 +185,7 @@ class TestMetaVars: workflow_job.workflow_nodes.create(job=job) data = job.awx_meta_vars() - for name in JOB_VARIABLE_PREFIXES: + for name in get_job_variable_prefixes(): assert data['{}_user_id'.format(name)] == admin_user.id assert data['{}_user_name'.format(name)] == admin_user.username assert data['{}_workflow_job_id'.format(name)] == workflow_job.pk @@ -189,7 +195,7 @@ class TestMetaVars: schedule = Schedule.objects.create(name='job-schedule', rrule='DTSTART:20171129T155939z\nFREQ=MONTHLY', unified_job_template=job_template) job = Job.objects.create(name='fake-job', launch_type='workflow', schedule=schedule, job_template=job_template) data = job.awx_meta_vars() - for name in JOB_VARIABLE_PREFIXES: + for name in get_job_variable_prefixes(): assert data['{}_schedule_id'.format(name)] == schedule.pk assert '{}_user_name'.format(name) not in data @@ -201,7 +207,7 @@ class TestMetaVars: job = Job.objects.create(launch_type='workflow') workflow_job.workflow_nodes.create(job=job) result_hash = {} - for name in JOB_VARIABLE_PREFIXES: + for name in get_job_variable_prefixes(): result_hash['{}_job_id'.format(name)] = job.id result_hash['{}_job_launch_type'.format(name)] = 'workflow' result_hash['{}_workflow_job_name'.format(name)] = 'workflow-job' diff --git a/awx/main/tests/unit/models/test_unified_job_unit.py b/awx/main/tests/unit/models/test_unified_job_unit.py index 2fa8807dff..54113184ba 100644 --- a/awx/main/tests/unit/models/test_unified_job_unit.py +++ b/awx/main/tests/unit/models/test_unified_job_unit.py @@ -1,7 +1,7 @@ from unittest import mock from awx.main.models import UnifiedJob, UnifiedJobTemplate, WorkflowJob, WorkflowJobNode, WorkflowApprovalTemplate, Job, User, Project, JobTemplate, Inventory -from awx.main.constants import JOB_VARIABLE_PREFIXES +from awx.main.utils.common import get_job_variable_prefixes def test_incorrectly_formatted_variables(): @@ -50,7 +50,7 @@ class TestMetaVars: maker = User(username='joe', pk=47, id=47) inv = Inventory(name='example-inv', id=45) result_hash = {} - for name in JOB_VARIABLE_PREFIXES: + for name in get_job_variable_prefixes(): result_hash['{}_job_id'.format(name)] = 42 result_hash['{}_job_launch_type'.format(name)] = 'manual' result_hash['{}_user_name'.format(name)] = 'joe' @@ -75,8 +75,48 @@ class TestMetaVars: project=Project(name='jobs-sync', scm_revision='12345444'), job_template=JobTemplate(name='jobs-jt', id=92, pk=92), ).awx_meta_vars() - for name in JOB_VARIABLE_PREFIXES: + for name in get_job_variable_prefixes(): assert data['{}_project_revision'.format(name)] == '12345444' assert '{}_job_template_id'.format(name) in data assert data['{}_job_template_id'.format(name)] == 92 assert data['{}_job_template_name'.format(name)] == 'jobs-jt' + + +class TestGetJobVariablePrefixes: + """Tests for the get_job_variable_prefixes() helper function.""" + + def test_default_returns_both(self): + from django.conf import settings + + with mock.patch.object(settings, 'INCLUDE_DEPRECATED_AWX_VAR_PREFIX', True, create=True): + assert get_job_variable_prefixes() == ['awx', 'tower'] + + def test_disabled_returns_tower_only(self): + from django.conf import settings + + with mock.patch.object(settings, 'INCLUDE_DEPRECATED_AWX_VAR_PREFIX', False, create=True): + assert get_job_variable_prefixes() == ['tower'] + + def test_fallback_when_setting_not_available(self): + """When setting is not available, falls back to both prefixes for backward compatibility.""" + fake_settings = mock.MagicMock(spec=[]) + with mock.patch('django.conf.settings', fake_settings): + assert get_job_variable_prefixes() == ['awx', 'tower'] + + def test_job_metavars_both_prefixes(self): + """With INCLUDE_DEPRECATED_AWX_VAR_PREFIX=True, both awx_ and tower_ variables.""" + from django.conf import settings + + with mock.patch.object(settings, 'INCLUDE_DEPRECATED_AWX_VAR_PREFIX', True, create=True): + data = Job(name='fake-job', pk=1, id=1, launch_type='manual').awx_meta_vars() + assert 'awx_job_id' in data + assert 'tower_job_id' in data + + def test_job_metavars_tower_only(self): + """With INCLUDE_DEPRECATED_AWX_VAR_PREFIX=False, only tower_ prefixed variables.""" + from django.conf import settings + + with mock.patch.object(settings, 'INCLUDE_DEPRECATED_AWX_VAR_PREFIX', False, create=True): + data = Job(name='fake-job', pk=1, id=1, launch_type='manual').awx_meta_vars() + assert 'tower_job_id' in data + assert 'awx_job_id' not in data diff --git a/awx/main/tests/unit/test_tasks.py b/awx/main/tests/unit/test_tasks.py index 62c7f42b0a..68b4d8e6d0 100644 --- a/awx/main/tests/unit/test_tasks.py +++ b/awx/main/tests/unit/test_tasks.py @@ -37,7 +37,7 @@ from awx.main.utils import encrypt_field, encrypt_value from awx.main.utils.safe_yaml import SafeLoader from awx.main.utils.licensing import Licenser -from awx.main.constants import JOB_VARIABLE_PREFIXES +from awx.main.utils.common import get_job_variable_prefixes from receptorctl.socket_interface import ReceptorControl @@ -372,12 +372,12 @@ class TestExtraVarSanitation(TestJobExecution): extra_vars = yaml.load(fd, Loader=SafeLoader) # ensure that strings are marked as unsafe - for name in JOB_VARIABLE_PREFIXES: + for name in get_job_variable_prefixes(): for variable_name in ['_job_template_name', '_user_name', '_job_launch_type', '_project_revision', '_inventory_name']: assert hasattr(extra_vars['{}{}'.format(name, variable_name)], '__UNSAFE__') # ensure that non-strings are marked as safe - for name in JOB_VARIABLE_PREFIXES: + for name in get_job_variable_prefixes(): for variable_name in ['_job_template_id', '_job_id', '_user_id', '_inventory_id']: assert not hasattr(extra_vars['{}{}'.format(name, variable_name)], '__UNSAFE__') @@ -524,7 +524,7 @@ class TestGenericRun: call_args, _ = task._write_extra_vars_file.call_args_list[0] private_data_dir, extra_vars, safe_dict = call_args - for name in JOB_VARIABLE_PREFIXES: + for name in get_job_variable_prefixes(): assert extra_vars['{}_user_id'.format(name)] == 123 assert extra_vars['{}_user_name'.format(name)] == "angry-spud" @@ -615,7 +615,7 @@ class TestAdhocRun(TestJobExecution): call_args, _ = task._write_extra_vars_file.call_args_list[0] private_data_dir, extra_vars = call_args - for name in JOB_VARIABLE_PREFIXES: + for name in get_job_variable_prefixes(): assert extra_vars['{}_user_id'.format(name)] == 123 assert extra_vars['{}_user_name'.format(name)] == "angry-spud" diff --git a/awx/main/utils/common.py b/awx/main/utils/common.py index a02daef166..9226aa05cd 100644 --- a/awx/main/utils/common.py +++ b/awx/main/utils/common.py @@ -93,6 +93,7 @@ __all__ = [ 'get_event_partition_epoch', 'cleanup_new_process', 'unified_job_class_to_event_table_name', + 'get_job_variable_prefixes', ] @@ -773,6 +774,21 @@ def get_cpu_effective_capacity(cpu_count, is_control_node=False): return max(1, int(cpu_count * forkcpu)) +def get_job_variable_prefixes(): + """Return the list of active job variable prefixes based on INCLUDE_DEPRECATED_AWX_VAR_PREFIX setting. + + When True (default), returns both 'awx' and 'tower' prefixes for backward compatibility. + When False, returns only 'tower'. The 'awx' prefix is deprecated and this setting + will default to False in a future release. + """ + from django.conf import settings + + include_awx = getattr(settings, 'INCLUDE_DEPRECATED_AWX_VAR_PREFIX', True) + if include_awx: + return ['awx', 'tower'] + return ['tower'] + + def convert_mem_str_to_bytes(mem_str): """Convert string with suffix indicating units to memory in bytes (base 2)