From 50b85d86337236a57cd5ae5db4f2b1d9b5bee44b Mon Sep 17 00:00:00 2001 From: Blai Peidro Date: Mon, 14 Sep 2026 08:39:32 +0200 Subject: [PATCH 1/2] refactor: give the settings #984 could not reach the Ascender name #984 renamed the eleven settings the database registers. Seventeen more were left, because they are not registered: thirteen are defined in defaults.py and overridden in a settings file, and four are read from the environment. The thirteen take the same route as #984: renamed at the definition and every use, with the old name added to the _FORMER_NAMES table so a conf.d file that writes AWX_NOTIFICATION_REQUEST_TIMEOUT still lands on the current name. The four from the environment could not, and this is the part worth reading. That table runs after the settings modules have loaded, and AWX_SETTINGS_FILE and AWX_SETTINGS_DIR decide which settings file loads at all, so an alias applied afterwards would be too late to matter. They read both names directly instead, through one helper, in the same shape as the ASCENDER_ variables ascender-kit took in its #72. Nothing has to move to the new names. AWX_SETTINGS_FILE is what the Dockerfile passes to collectstatic, AWX_LOGGING_MODE is what the three installers write into their compose environments, and AWX_WEB_PROCESS is what supervisor sets. Those keep working for as long as they exist rather than for a release. A test asserts that no settings module reads one of the four with a bare os.environ.get, since that would answer to one name only, which is the failure the helper exists to prevent. Both directions are checked against a real production settings load: the old environment name still selects the settings file, and a file written with the old setting name still carries over. --- awx/main/dispatch/worker/callback.py | 2 +- .../management/commands/provision_instance.py | 2 +- awx/main/managers.py | 2 +- awx/main/models/ha.py | 2 +- awx/main/models/inventory.py | 4 +- awx/main/notifications/grafana_backend.py | 2 +- awx/main/notifications/mattermost_backend.py | 2 +- awx/main/notifications/pagerduty_backend.py | 2 +- awx/main/notifications/rocketchat_backend.py | 2 +- awx/main/notifications/twilio_backend.py | 2 +- awx/main/notifications/webhook_backend.py | 2 +- awx/main/scheduler/kubernetes.py | 2 +- awx/main/scheduler/task_manager.py | 2 +- awx/main/scheduler/task_manager_models.py | 8 +-- awx/main/tasks/jobs.py | 4 +- awx/main/tasks/receptor.py | 2 +- awx/main/tasks/system.py | 6 +- .../commands/test_provision_instance.py | 2 +- .../task_management/test_container_groups.py | 2 +- .../task_management/test_scheduler.py | 4 +- .../tests/unit/notifications/test_grafana.py | 14 ++-- .../unit/notifications/test_mattermost.py | 2 +- .../unit/notifications/test_rocketchat.py | 4 +- .../tests/unit/notifications/test_webhook.py | 16 ++--- .../unit/settings/test_environment_names.py | 69 +++++++++++++++++++ .../tests/unit/settings/test_logging_mode.py | 4 +- awx/main/utils/execution_environments.py | 4 +- awx/settings/connection_reuse.py | 4 +- awx/settings/defaults.py | 31 +++++---- awx/settings/development.py | 2 +- awx/settings/environment.py | 32 +++++++++ awx/settings/production.py | 20 +++++- awx/settings/statement_timeout.py | 4 +- awx/settings/typed.py | 26 +++---- docs/capacity.md | 6 +- docs/deprecated/inventory_refresh.md | 2 +- docs/docsite/rst/userguide/inventories.rst | 2 +- docs/tasks.md | 2 +- 38 files changed, 208 insertions(+), 92 deletions(-) create mode 100644 awx/main/tests/unit/settings/test_environment_names.py create mode 100644 awx/settings/environment.py diff --git a/awx/main/dispatch/worker/callback.py b/awx/main/dispatch/worker/callback.py index 2c3c92a12..0d8c59d30 100644 --- a/awx/main/dispatch/worker/callback.py +++ b/awx/main/dispatch/worker/callback.py @@ -140,7 +140,7 @@ def toggle_profiling(self, *args): logger.error(f'profiling is disabled, wrote {filepath}') def work_loop(self, *args, **kw): - if settings.AWX_CALLBACK_PROFILE: + if settings.ASCENDER_CALLBACK_PROFILE: signal.signal(signal.SIGUSR1, self.toggle_profiling) return super(CallbackBrokerWorker, self).work_loop(*args, **kw) diff --git a/awx/main/management/commands/provision_instance.py b/awx/main/management/commands/provision_instance.py index 5b208b209..00572701a 100644 --- a/awx/main/management/commands/provision_instance.py +++ b/awx/main/management/commands/provision_instance.py @@ -30,7 +30,7 @@ def add_arguments(self, parser): def _register_hostname(self, hostname, node_type, uuid): if not hostname: - if not settings.AWX_AUTO_DEPROVISION_INSTANCES: + if not settings.ASCENDER_AUTO_DEPROVISION_INSTANCES: raise CommandError('Registering with values from settings only intended for use in K8s installs') from awx.main.management.commands.register_queue import RegisterQueue diff --git a/awx/main/managers.py b/awx/main/managers.py index 7bffc008a..574a7e2e0 100644 --- a/awx/main/managers.py +++ b/awx/main/managers.py @@ -197,7 +197,7 @@ def register( ip_address = "" with advisory_lock('instance_registration_%s' % hostname): - if settings.AWX_AUTO_DEPROVISION_INSTANCES: + if settings.ASCENDER_AUTO_DEPROVISION_INSTANCES: # detect any instances with the same IP address. # if one exists, set it to "" if ip_address: diff --git a/awx/main/models/ha.py b/awx/main/models/ha.py index e1d1b8ee6..f9415748b 100644 --- a/awx/main/models/ha.py +++ b/awx/main/models/ha.py @@ -211,7 +211,7 @@ def consumed_capacity(self): ) if self.node_type in ('hybrid', 'control'): capacity_consumed += ( - settings.AWX_CONTROL_NODE_TASK_IMPACT * UnifiedJob.objects.filter(controller_node=self.hostname, status__in=('running', 'waiting')).count() + settings.ASCENDER_CONTROL_NODE_TASK_IMPACT * UnifiedJob.objects.filter(controller_node=self.hostname, status__in=('running', 'waiting')).count() ) return capacity_consumed diff --git a/awx/main/models/inventory.py b/awx/main/models/inventory.py index aac226a53..8bbf4e4af 100644 --- a/awx/main/models/inventory.py +++ b/awx/main/models/inventory.py @@ -475,7 +475,7 @@ def schedule_deletion(self, user_id=None): delete_inventory.delay(self.pk, user_id) def _update_host_smart_inventory_memeberships(self): - if self.kind == 'smart' and settings.AWX_REBUILD_SMART_MEMBERSHIP: + if self.kind == 'smart' and settings.ASCENDER_REBUILD_SMART_MEMBERSHIP: def on_commit(): from awx.main.tasks.system import update_host_smart_inventory_memberships @@ -649,7 +649,7 @@ def get_effective_host_name(self): return host_name def _update_host_smart_inventory_memeberships(self): - if settings.AWX_REBUILD_SMART_MEMBERSHIP: + if settings.ASCENDER_REBUILD_SMART_MEMBERSHIP: def on_commit(): from awx.main.tasks.system import update_host_smart_inventory_memberships diff --git a/awx/main/notifications/grafana_backend.py b/awx/main/notifications/grafana_backend.py index 260664f2b..c74b4c359 100644 --- a/awx/main/notifications/grafana_backend.py +++ b/awx/main/notifications/grafana_backend.py @@ -107,7 +107,7 @@ def send_messages(self, messages): json=grafana_data, headers=grafana_headers, verify=(not self.grafana_no_verify_ssl), - timeout=settings.AWX_NOTIFICATION_REQUEST_TIMEOUT, + timeout=settings.ASCENDER_NOTIFICATION_REQUEST_TIMEOUT, ) if r.status_code >= 400: logger.error(smart_str(_("Error sending notification grafana: {}").format(r.status_code))) diff --git a/awx/main/notifications/mattermost_backend.py b/awx/main/notifications/mattermost_backend.py index 3b84ae464..6ef9b64b5 100644 --- a/awx/main/notifications/mattermost_backend.py +++ b/awx/main/notifications/mattermost_backend.py @@ -49,7 +49,7 @@ def send_messages(self, messages): "{}".format(m.recipients()[0]), json=payload, verify=(not self.mattermost_no_verify_ssl), - timeout=settings.AWX_NOTIFICATION_REQUEST_TIMEOUT, + timeout=settings.ASCENDER_NOTIFICATION_REQUEST_TIMEOUT, ) if r.status_code >= 400: logger.error(smart_str(_("Error sending notification mattermost: {}").format(r.status_code))) diff --git a/awx/main/notifications/pagerduty_backend.py b/awx/main/notifications/pagerduty_backend.py index aa9d65eae..8295e0b48 100644 --- a/awx/main/notifications/pagerduty_backend.py +++ b/awx/main/notifications/pagerduty_backend.py @@ -96,7 +96,7 @@ def send_messages(self, messages): "client": m.from_email, }, headers=get_awx_http_client_headers(), - timeout=settings.AWX_NOTIFICATION_REQUEST_TIMEOUT, + timeout=settings.ASCENDER_NOTIFICATION_REQUEST_TIMEOUT, ) r.raise_for_status() sent_messages += 1 diff --git a/awx/main/notifications/rocketchat_backend.py b/awx/main/notifications/rocketchat_backend.py index b4fa5c265..9a0524c0b 100644 --- a/awx/main/notifications/rocketchat_backend.py +++ b/awx/main/notifications/rocketchat_backend.py @@ -47,7 +47,7 @@ def send_messages(self, messages): data=json.dumps(payload), headers=get_awx_http_client_headers(), verify=(not self.rocketchat_no_verify_ssl), - timeout=settings.AWX_NOTIFICATION_REQUEST_TIMEOUT, + timeout=settings.ASCENDER_NOTIFICATION_REQUEST_TIMEOUT, ) if r.status_code >= 400: diff --git a/awx/main/notifications/twilio_backend.py b/awx/main/notifications/twilio_backend.py index a3fa0e5f7..de9e2d171 100644 --- a/awx/main/notifications/twilio_backend.py +++ b/awx/main/notifications/twilio_backend.py @@ -51,7 +51,7 @@ def send_messages(self, messages): auth=(self.account_sid, self.account_token), data={"To": dest, "From": m.from_email, "Body": m.subject}, headers=get_awx_http_client_headers(), - timeout=settings.AWX_NOTIFICATION_REQUEST_TIMEOUT, + timeout=settings.ASCENDER_NOTIFICATION_REQUEST_TIMEOUT, ) r.raise_for_status() sent_messages += 1 diff --git a/awx/main/notifications/webhook_backend.py b/awx/main/notifications/webhook_backend.py index 7f6d9d1e9..d6e8953af 100644 --- a/awx/main/notifications/webhook_backend.py +++ b/awx/main/notifications/webhook_backend.py @@ -95,7 +95,7 @@ def send_messages(self, messages): headers=headers, verify=(not self.disable_ssl_verification), allow_redirects=False, # override default behaviour for redirects - timeout=settings.AWX_NOTIFICATION_REQUEST_TIMEOUT, + timeout=settings.ASCENDER_NOTIFICATION_REQUEST_TIMEOUT, ) # either success or error reached if this conditional fires diff --git a/awx/main/scheduler/kubernetes.py b/awx/main/scheduler/kubernetes.py index 31d94b539..5bca6fb6d 100644 --- a/awx/main/scheduler/kubernetes.py +++ b/awx/main/scheduler/kubernetes.py @@ -157,7 +157,7 @@ def kube_api(self): # in Configuration.__init__. Container group API traffic was never proxied # before that, and on OpenShift the injected cluster-wide proxy makes TLS # verification against the cluster CA fail, so this stays opt-in. - if not settings.AWX_CONTAINER_GROUP_K8S_API_USE_PROXY: + if not settings.ASCENDER_CONTAINER_GROUP_K8S_API_USE_PROXY: cfg.proxy = None cfg.proxy_headers = None diff --git a/awx/main/scheduler/task_manager.py b/awx/main/scheduler/task_manager.py index 6f96ac582..b168e48bf 100644 --- a/awx/main/scheduler/task_manager.py +++ b/awx/main/scheduler/task_manager.py @@ -74,7 +74,7 @@ def __init__(self, prefix=""): # We want to avoid calling settings in loops, so cache these settings at init time self.start_task_limit = settings.START_TASK_LIMIT self.task_manager_timeout = settings.TASK_MANAGER_TIMEOUT - self.control_task_impact = settings.AWX_CONTROL_NODE_TASK_IMPACT + self.control_task_impact = settings.ASCENDER_CONTROL_NODE_TASK_IMPACT for m in self.subsystem_metrics.METRICS: if m.startswith(self.prefix): diff --git a/awx/main/scheduler/task_manager_models.py b/awx/main/scheduler/task_manager_models.py index 0aa68e5ea..d9c51384b 100644 --- a/awx/main/scheduler/task_manager_models.py +++ b/awx/main/scheduler/task_manager_models.py @@ -51,7 +51,7 @@ def __init__(self, obj, task_manager_instances=None, **kwargs): self.instance_hostnames = tuple([instance.hostname for instance in _instances if instance.hostname in task_manager_instances]) self.max_concurrent_jobs = obj.max_concurrent_jobs self.max_forks = obj.max_forks - self.control_task_impact = kwargs.get('control_task_impact', settings.AWX_CONTROL_NODE_TASK_IMPACT) + self.control_task_impact = kwargs.get('control_task_impact', settings.ASCENDER_CONTROL_NODE_TASK_IMPACT) def consume_capacity(self, task): """We only consume capacity on an instance group level if it is a container group. Otherwise we consume capacity on an instance level.""" @@ -134,7 +134,7 @@ def __init__(self, instances=None, instance_fields=('node_type', 'capacity', 'ho self.instances_by_hostname = dict() self.instance_groups_container_group_jobs = dict() self.instance_groups_container_group_consumed_forks = dict() - self.control_task_impact = kwargs.get('control_task_impact', settings.AWX_CONTROL_NODE_TASK_IMPACT) + self.control_task_impact = kwargs.get('control_task_impact', settings.ASCENDER_CONTROL_NODE_TASK_IMPACT) if instances is None: instances = ( @@ -170,7 +170,7 @@ def __init__(self, task_manager_instances=None, instance_groups=None, instance_g self.task_manager_instances = task_manager_instances if task_manager_instances is not None else TaskManagerInstances() self.controlplane_ig = None self.pk_ig_map = dict() - self.control_task_impact = kwargs.get('control_task_impact', settings.AWX_CONTROL_NODE_TASK_IMPACT) + self.control_task_impact = kwargs.get('control_task_impact', settings.ASCENDER_CONTROL_NODE_TASK_IMPACT) self.controlplane_ig_name = kwargs.get('controlplane_ig_name', settings.DEFAULT_CONTROL_PLANE_QUEUE_NAME) if instance_groups is not None: # for testing @@ -265,7 +265,7 @@ def get_instance_groups_from_task_cache(self, task): class TaskManagerModels: def __init__(self, **kwargs): # We want to avoid calls to settings over and over in loops, so cache this information here - kwargs['control_task_impact'] = kwargs.get('control_task_impact', settings.AWX_CONTROL_NODE_TASK_IMPACT) + kwargs['control_task_impact'] = kwargs.get('control_task_impact', settings.ASCENDER_CONTROL_NODE_TASK_IMPACT) kwargs['controlplane_ig_name'] = kwargs.get('controlplane_ig_name', settings.DEFAULT_CONTROL_PLANE_QUEUE_NAME) self.instances = TaskManagerInstances(**kwargs) self.instance_groups = TaskManagerInstanceGroups(task_manager_instances=self.instances, **kwargs) diff --git a/awx/main/tasks/jobs.py b/awx/main/tasks/jobs.py index f0eb498cf..4fbd97982 100644 --- a/awx/main/tasks/jobs.py +++ b/awx/main/tasks/jobs.py @@ -572,7 +572,7 @@ def run(self, pk, **kwargs): 'playbook': self.build_playbook_path_relative_to_cwd(self.instance, private_data_dir), 'inventory': self.build_inventory(self.instance, private_data_dir), 'passwords': expect_passwords, - 'suppress_env_files': getattr(settings, 'AWX_RUNNER_OMIT_ENV_FILES', True), + 'suppress_env_files': getattr(settings, 'ASCENDER_RUNNER_OMIT_ENV_FILES', True), 'envvars': env, } @@ -604,7 +604,7 @@ def run(self, pk, **kwargs): runner_settings = { 'job_timeout': self.get_instance_timeout(self.instance), 'suppress_ansible_output': True, - 'suppress_output_file': getattr(settings, 'AWX_RUNNER_SUPPRESS_OUTPUT_FILE', True), + 'suppress_output_file': getattr(settings, 'ASCENDER_RUNNER_SUPPRESS_OUTPUT_FILE', True), } idle_timeout = getattr(settings, 'DEFAULT_JOB_IDLE_TIMEOUT', 0) diff --git a/awx/main/tasks/receptor.py b/awx/main/tasks/receptor.py index 74ebe6f76..81d54671d 100644 --- a/awx/main/tasks/receptor.py +++ b/awx/main/tasks/receptor.py @@ -475,7 +475,7 @@ def receptor_params(self): receptor_params = { "secret_kube_pod": spec_yaml, - "pod_pending_timeout": getattr(settings, 'AWX_CONTAINER_GROUP_POD_PENDING_TIMEOUT', "5m"), + "pod_pending_timeout": getattr(settings, 'ASCENDER_CONTAINER_GROUP_POD_PENDING_TIMEOUT', "5m"), } if self.credential: diff --git a/awx/main/tasks/system.py b/awx/main/tasks/system.py index 68c0eb55f..125d14eba 100644 --- a/awx/main/tasks/system.py +++ b/awx/main/tasks/system.py @@ -598,7 +598,7 @@ def cluster_node_heartbeat(dispatch_time=None, worker_tasks=None): elif (nowtime - last_last_seen) > timedelta(seconds=settings.CLUSTER_NODE_HEARTBEAT_PERIOD + 2): logger.warning(f'Heartbeat skew - interval={(nowtime - last_last_seen).total_seconds():.4f}, expected={settings.CLUSTER_NODE_HEARTBEAT_PERIOD}') else: - if settings.AWX_AUTO_DEPROVISION_INSTANCES: + if settings.ASCENDER_AUTO_DEPROVISION_INSTANCES: changed, this_inst = Instance.objects.register(ip_address=os.environ.get('MY_POD_IP'), node_type='control', node_uuid=settings.SYSTEM_UUID) if changed: logger.warning(f'Recreated instance record {this_inst.hostname} after unexpected removal') @@ -630,7 +630,7 @@ def cluster_node_heartbeat(dispatch_time=None, worker_tasks=None): except Exception: logger.exception('failed to reap jobs for {}'.format(other_inst.hostname)) try: - if settings.AWX_AUTO_DEPROVISION_INSTANCES and other_inst.node_type == "control": + if settings.ASCENDER_AUTO_DEPROVISION_INSTANCES and other_inst.node_type == "control": deprovision_hostname = other_inst.hostname other_inst.delete() # FIXME: what about associated inbound links? logger.info("Host {} Automatically Deprovisioned.".format(deprovision_hostname)) @@ -713,7 +713,7 @@ def awx_k8s_reaper(): logger.debug('{} is no longer active, reaping orphaned k8s pod'.format(job.log_format)) try: pm = PodManager(job) - pm.kube_api.delete_namespaced_pod(name=pods[job.id], namespace=pm.namespace, _request_timeout=settings.AWX_CONTAINER_GROUP_K8S_API_TIMEOUT) + pm.kube_api.delete_namespaced_pod(name=pods[job.id], namespace=pm.namespace, _request_timeout=settings.ASCENDER_CONTAINER_GROUP_K8S_API_TIMEOUT) except Exception: logger.exception("Failed to delete orphaned pod {} from {}".format(job.log_format, group)) diff --git a/awx/main/tests/functional/commands/test_provision_instance.py b/awx/main/tests/functional/commands/test_provision_instance.py index bb0231863..8015ed7e8 100644 --- a/awx/main/tests/functional/commands/test_provision_instance.py +++ b/awx/main/tests/functional/commands/test_provision_instance.py @@ -27,7 +27,7 @@ def test_register_self_openshift(): assert not Instance.objects.exists() assert not InstanceGroup.objects.exists() - with override_settings(AWX_AUTO_DEPROVISION_INSTANCES=True, CLUSTER_HOST_ID='foo_node', SYSTEM_UUID='12345'): + with override_settings(ASCENDER_AUTO_DEPROVISION_INSTANCES=True, CLUSTER_HOST_ID='foo_node', SYSTEM_UUID='12345'): Command().handle() inst = Instance.objects.first() assert inst.hostname == 'foo_node' diff --git a/awx/main/tests/functional/task_management/test_container_groups.py b/awx/main/tests/functional/task_management/test_container_groups.py index 191da083d..9d1582ddf 100644 --- a/awx/main/tests/functional/task_management/test_container_groups.py +++ b/awx/main/tests/functional/task_management/test_container_groups.py @@ -154,5 +154,5 @@ def test_kube_api_ignores_proxy_environment(containerized_job, default_job_execu assert pm.kube_api.api_client.configuration.proxy is None del pm.__dict__['kube_api'] # drop the cached_property - with override_settings(AWX_CONTAINER_GROUP_K8S_API_USE_PROXY=True): + with override_settings(ASCENDER_CONTAINER_GROUP_K8S_API_USE_PROXY=True): assert pm.kube_api.api_client.configuration.proxy == 'http://proxy.example.com:3128' diff --git a/awx/main/tests/functional/task_management/test_scheduler.py b/awx/main/tests/functional/task_management/test_scheduler.py index 34e0dd3cb..ace75f531 100644 --- a/awx/main/tests/functional/task_management/test_scheduler.py +++ b/awx/main/tests/functional/task_management/test_scheduler.py @@ -237,7 +237,7 @@ def test_job_fails_to_launch_when_no_control_capacity(self, job_template, contro def test_hybrid_capacity(self, job_template, hybrid_instance): enough_capacity = job_template.create_unified_job() insufficient_capacity = job_template.create_unified_job() - expected_task_impact = enough_capacity.task_impact + settings.AWX_CONTROL_NODE_TASK_IMPACT + expected_task_impact = enough_capacity.task_impact + settings.ASCENDER_CONTROL_NODE_TASK_IMPACT all_ujs = [enough_capacity, insufficient_capacity] for uj in all_ujs: uj.signal_start() @@ -261,7 +261,7 @@ def test_hybrid_capacity(self, job_template, hybrid_instance): def test_project_update_capacity(self, project, hybrid_instance, instance_group_factory, controlplane_instance_group): pu = project.create_unified_job() instance_group_factory(name='second_ig', instances=[hybrid_instance]) - expected_task_impact = pu.task_impact + settings.AWX_CONTROL_NODE_TASK_IMPACT + expected_task_impact = pu.task_impact + settings.ASCENDER_CONTROL_NODE_TASK_IMPACT pu.signal_start() tm = TaskManager() diff --git a/awx/main/tests/unit/notifications/test_grafana.py b/awx/main/tests/unit/notifications/test_grafana.py index 260bcba53..fc43cbe5e 100644 --- a/awx/main/tests/unit/notifications/test_grafana.py +++ b/awx/main/tests/unit/notifications/test_grafana.py @@ -33,7 +33,7 @@ def test_send_messages(): headers={'Content-Type': 'application/json', 'Authorization': 'Bearer testapikey'}, json={'text': 'test subject', 'isRegion': True, 'timeEnd': 120000, 'time': 60000}, verify=True, - timeout=settings.AWX_NOTIFICATION_REQUEST_TIMEOUT, + timeout=settings.ASCENDER_NOTIFICATION_REQUEST_TIMEOUT, ) assert sent_messages == 1 @@ -64,7 +64,7 @@ def test_send_messages_with_no_verify_ssl(): headers={'Content-Type': 'application/json', 'Authorization': 'Bearer testapikey'}, json={'text': 'test subject', 'isRegion': True, 'timeEnd': 120000, 'time': 60000}, verify=False, - timeout=settings.AWX_NOTIFICATION_REQUEST_TIMEOUT, + timeout=settings.ASCENDER_NOTIFICATION_REQUEST_TIMEOUT, ) assert sent_messages == 1 @@ -96,7 +96,7 @@ def test_send_messages_with_dashboardid(dashboardId): headers={'Content-Type': 'application/json', 'Authorization': 'Bearer testapikey'}, json={'text': 'test subject', 'isRegion': True, 'timeEnd': 120000, 'time': 60000, 'dashboardId': dashboardId}, verify=True, - timeout=settings.AWX_NOTIFICATION_REQUEST_TIMEOUT, + timeout=settings.ASCENDER_NOTIFICATION_REQUEST_TIMEOUT, ) assert sent_messages == 1 @@ -128,7 +128,7 @@ def test_send_messages_with_panelid(panelId): headers={'Content-Type': 'application/json', 'Authorization': 'Bearer testapikey'}, json={'text': 'test subject', 'isRegion': True, 'timeEnd': 120000, 'panelId': int(panelId), 'time': 60000}, verify=True, - timeout=settings.AWX_NOTIFICATION_REQUEST_TIMEOUT, + timeout=settings.ASCENDER_NOTIFICATION_REQUEST_TIMEOUT, ) assert sent_messages == 1 @@ -159,7 +159,7 @@ def test_send_messages_with_bothids(): headers={'Content-Type': 'application/json', 'Authorization': 'Bearer testapikey'}, json={'text': 'test subject', 'isRegion': True, 'timeEnd': 120000, 'panelId': 42, 'time': 60000, 'dashboardId': 42}, verify=True, - timeout=settings.AWX_NOTIFICATION_REQUEST_TIMEOUT, + timeout=settings.ASCENDER_NOTIFICATION_REQUEST_TIMEOUT, ) assert sent_messages == 1 @@ -190,7 +190,7 @@ def test_send_messages_with_emptyids(): headers={'Content-Type': 'application/json', 'Authorization': 'Bearer testapikey'}, json={'text': 'test subject', 'isRegion': True, 'timeEnd': 120000, 'time': 60000}, verify=True, - timeout=settings.AWX_NOTIFICATION_REQUEST_TIMEOUT, + timeout=settings.ASCENDER_NOTIFICATION_REQUEST_TIMEOUT, ) assert sent_messages == 1 @@ -221,6 +221,6 @@ def test_send_messages_with_tags(): headers={'Content-Type': 'application/json', 'Authorization': 'Bearer testapikey'}, json={'tags': ['ansible'], 'text': 'test subject', 'isRegion': True, 'timeEnd': 120000, 'time': 60000}, verify=True, - timeout=settings.AWX_NOTIFICATION_REQUEST_TIMEOUT, + timeout=settings.ASCENDER_NOTIFICATION_REQUEST_TIMEOUT, ) assert sent_messages == 1 diff --git a/awx/main/tests/unit/notifications/test_mattermost.py b/awx/main/tests/unit/notifications/test_mattermost.py index 9c8e78e16..92a6b1edb 100644 --- a/awx/main/tests/unit/notifications/test_mattermost.py +++ b/awx/main/tests/unit/notifications/test_mattermost.py @@ -29,6 +29,6 @@ def test_send_messages(): 'http://example.com', json={'text': 'test subject'}, verify=True, - timeout=settings.AWX_NOTIFICATION_REQUEST_TIMEOUT, + timeout=settings.ASCENDER_NOTIFICATION_REQUEST_TIMEOUT, ) assert sent_messages == 1 diff --git a/awx/main/tests/unit/notifications/test_rocketchat.py b/awx/main/tests/unit/notifications/test_rocketchat.py index f004273ef..f9763d6b6 100644 --- a/awx/main/tests/unit/notifications/test_rocketchat.py +++ b/awx/main/tests/unit/notifications/test_rocketchat.py @@ -33,7 +33,7 @@ def test_send_messages(): data='{"text": "test subject"}', headers={'Content-Type': 'application/json', 'User-Agent': 'AWX 0.0.1.dev (open)'}, verify=True, - timeout=settings.AWX_NOTIFICATION_REQUEST_TIMEOUT, + timeout=settings.ASCENDER_NOTIFICATION_REQUEST_TIMEOUT, ) assert sent_messages == 1 @@ -120,6 +120,6 @@ def test_send_messages_with_no_verify_ssl(): data='{"text": "test subject"}', headers={'Content-Type': 'application/json', 'User-Agent': 'AWX 0.0.1.dev (open)'}, verify=False, - timeout=settings.AWX_NOTIFICATION_REQUEST_TIMEOUT, + timeout=settings.ASCENDER_NOTIFICATION_REQUEST_TIMEOUT, ) assert sent_messages == 1 diff --git a/awx/main/tests/unit/notifications/test_webhook.py b/awx/main/tests/unit/notifications/test_webhook.py index 2b11515a3..5e7e18c02 100644 --- a/awx/main/tests/unit/notifications/test_webhook.py +++ b/awx/main/tests/unit/notifications/test_webhook.py @@ -34,7 +34,7 @@ def test_send_messages_as_POST(): data=json.dumps({'text': 'test body'}, ensure_ascii=False).encode('utf-8'), headers={'Content-Type': 'application/json', 'User-Agent': 'AWX 0.0.1.dev (open)'}, verify=True, - timeout=settings.AWX_NOTIFICATION_REQUEST_TIMEOUT, + timeout=settings.ASCENDER_NOTIFICATION_REQUEST_TIMEOUT, allow_redirects=False, ) assert sent_messages == 1 @@ -67,7 +67,7 @@ def test_send_messages_as_PUT(): data=json.dumps({'text': 'test body 2'}, ensure_ascii=False).encode('utf-8'), headers={'Content-Type': 'application/json', 'User-Agent': 'AWX 0.0.1.dev (open)'}, verify=True, - timeout=settings.AWX_NOTIFICATION_REQUEST_TIMEOUT, + timeout=settings.ASCENDER_NOTIFICATION_REQUEST_TIMEOUT, allow_redirects=False, ) assert sent_messages == 1 @@ -100,7 +100,7 @@ def test_send_messages_with_username(): data=json.dumps({'text': 'test body'}, ensure_ascii=False).encode('utf-8'), headers={'Content-Type': 'application/json', 'User-Agent': 'AWX 0.0.1.dev (open)'}, verify=True, - timeout=settings.AWX_NOTIFICATION_REQUEST_TIMEOUT, + timeout=settings.ASCENDER_NOTIFICATION_REQUEST_TIMEOUT, allow_redirects=False, ) assert sent_messages == 1 @@ -133,7 +133,7 @@ def test_send_messages_with_password(): data=json.dumps({'text': 'test body'}, ensure_ascii=False).encode('utf-8'), headers={'Content-Type': 'application/json', 'User-Agent': 'AWX 0.0.1.dev (open)'}, verify=True, - timeout=settings.AWX_NOTIFICATION_REQUEST_TIMEOUT, + timeout=settings.ASCENDER_NOTIFICATION_REQUEST_TIMEOUT, allow_redirects=False, ) assert sent_messages == 1 @@ -166,7 +166,7 @@ def test_send_messages_with_username_and_password(): data=json.dumps({'text': 'test body'}, ensure_ascii=False).encode('utf-8'), headers={'Content-Type': 'application/json', 'User-Agent': 'AWX 0.0.1.dev (open)'}, verify=True, - timeout=settings.AWX_NOTIFICATION_REQUEST_TIMEOUT, + timeout=settings.ASCENDER_NOTIFICATION_REQUEST_TIMEOUT, allow_redirects=False, ) assert sent_messages == 1 @@ -199,7 +199,7 @@ def test_send_messages_with_no_verify_ssl(): data=json.dumps({'text': 'test body'}, ensure_ascii=False).encode('utf-8'), headers={'Content-Type': 'application/json', 'User-Agent': 'AWX 0.0.1.dev (open)'}, verify=False, - timeout=settings.AWX_NOTIFICATION_REQUEST_TIMEOUT, + timeout=settings.ASCENDER_NOTIFICATION_REQUEST_TIMEOUT, allow_redirects=False, ) assert sent_messages == 1 @@ -237,7 +237,7 @@ def test_send_messages_with_additional_headers(): 'X-Test-Header2': 'test-content-2', }, verify=True, - timeout=settings.AWX_NOTIFICATION_REQUEST_TIMEOUT, + timeout=settings.ASCENDER_NOTIFICATION_REQUEST_TIMEOUT, allow_redirects=False, ) assert sent_messages == 1 @@ -276,7 +276,7 @@ def test_send_messages_with_redirects_ok(): data=json.dumps({'text': 'test body'}, ensure_ascii=False).encode('utf-8'), headers={'Content-Type': 'application/json', 'User-Agent': 'AWX 0.0.1.dev (open)'}, verify=True, - timeout=settings.AWX_NOTIFICATION_REQUEST_TIMEOUT, + timeout=settings.ASCENDER_NOTIFICATION_REQUEST_TIMEOUT, allow_redirects=False, ) assert sent_messages == 1 diff --git a/awx/main/tests/unit/settings/test_environment_names.py b/awx/main/tests/unit/settings/test_environment_names.py new file mode 100644 index 000000000..5b0e04535 --- /dev/null +++ b/awx/main/tests/unit/settings/test_environment_names.py @@ -0,0 +1,69 @@ +# Copyright (c) 2026 Ascender +# All Rights Reserved. +"""Settings the platform reads from the environment rather than a settings file. + +Four of them, and the alias table in production.py cannot reach any: it runs +after the settings modules have loaded, and two of these decide which settings +file loads at all. So they are read under both names directly. + +The AWX names are what the image, the three installers and anyone's own start +script pass today, which is why they keep answering rather than being given a +release to move in. +""" + +import pytest + +from awx.settings.environment import environment_setting + + +@pytest.fixture +def clean_env(monkeypatch): + for prefix in ('ASCENDER_', 'AWX_'): + monkeypatch.delenv(prefix + 'LOGGING_MODE', raising=False) + return monkeypatch + + +def test_the_ascender_name_is_read(clean_env): + clean_env.setenv('ASCENDER_LOGGING_MODE', 'stdout') + + assert environment_setting('LOGGING_MODE', 'file') == 'stdout' + + +def test_the_awx_name_is_still_read(clean_env): + """What the image and the installers pass today.""" + clean_env.setenv('AWX_LOGGING_MODE', 'stdout') + + assert environment_setting('LOGGING_MODE', 'file') == 'stdout' + + +def test_the_ascender_name_wins(clean_env): + clean_env.setenv('AWX_LOGGING_MODE', 'stdout') + clean_env.setenv('ASCENDER_LOGGING_MODE', 'file') + + assert environment_setting('LOGGING_MODE', 'file') == 'file' + + +def test_neither_set_falls_back(clean_env): + assert environment_setting('LOGGING_MODE', 'file') == 'file' + + +def test_a_name_that_is_set_but_empty_still_wins(clean_env): + """Exporting an empty value is how a deployment says "not this", and + treating it as unset would quietly ignore that. + """ + clean_env.setenv('ASCENDER_LOGGING_MODE', '') + + assert environment_setting('LOGGING_MODE', 'file') == '' + + +@pytest.mark.parametrize('suffix', ['SETTINGS_FILE', 'SETTINGS_DIR', 'LOGGING_MODE', 'WEB_PROCESS']) +def test_every_environment_setting_is_read_through_the_helper(suffix): + """A direct os.environ.get for one of these would answer to one name only, + which is the bug this file exists to keep out. + """ + import pathlib + + settings_dir = pathlib.Path(environment_setting.__module__.replace('.', '/')).parent + sources = ' '.join(p.read_text() for p in pathlib.Path(settings_dir).glob('*.py') if p.name != 'environment.py') + + assert f"'AWX_{suffix}'" not in sources, f'AWX_{suffix} is read directly rather than through environment_setting' diff --git a/awx/main/tests/unit/settings/test_logging_mode.py b/awx/main/tests/unit/settings/test_logging_mode.py index 8197dcca9..4949990f2 100644 --- a/awx/main/tests/unit/settings/test_logging_mode.py +++ b/awx/main/tests/unit/settings/test_logging_mode.py @@ -3,7 +3,7 @@ """ Where the logs go, which is half of one process per container. -AWX_LOGGING_MODE decides it: 'file' writes to /var/log/tower, which is what a +ASCENDER_LOGGING_MODE decides it: 'file' writes to /var/log/tower, which is what a VM install wants, and 'stdout' lets the container runtime collect them. The images set stdout, and these hold the settings to that so a handler added later cannot quietly start writing a file nobody reads inside a container. @@ -61,5 +61,5 @@ def test_the_console_is_open_in_the_container_and_gated_on_a_vm(): def test_the_mode_is_checked_rather_than_assumed(): - with pytest.raises(Exception, match="AWX_LOGGING_MODE must be 'file' or 'stdout'"): + with pytest.raises(Exception, match="ASCENDER_LOGGING_MODE must be 'file' or 'stdout'"): build_logging('syslog') diff --git a/awx/main/utils/execution_environments.py b/awx/main/utils/execution_environments.py index fd70b75e9..700999f32 100644 --- a/awx/main/utils/execution_environments.py +++ b/awx/main/utils/execution_environments.py @@ -29,7 +29,7 @@ def get_default_execution_environment(): def get_default_pod_spec(): - job_label: str = settings.AWX_CONTAINER_GROUP_DEFAULT_JOB_LABEL + job_label: str = settings.ASCENDER_CONTAINER_GROUP_DEFAULT_JOB_LABEL ee = get_default_execution_environment() if ee is None: raise RuntimeError("Unable to find an execution environment.") @@ -37,7 +37,7 @@ def get_default_pod_spec(): return { "apiVersion": "v1", "kind": "Pod", - "metadata": {"namespace": settings.AWX_CONTAINER_GROUP_DEFAULT_NAMESPACE, "labels": {job_label: ""}}, + "metadata": {"namespace": settings.ASCENDER_CONTAINER_GROUP_DEFAULT_NAMESPACE, "labels": {job_label: ""}}, "spec": { "serviceAccountName": "default", "automountServiceAccountToken": False, diff --git a/awx/settings/connection_reuse.py b/awx/settings/connection_reuse.py index ee388a812..b7043d7d8 100644 --- a/awx/settings/connection_reuse.py +++ b/awx/settings/connection_reuse.py @@ -1,4 +1,4 @@ -import os +from awx.settings.environment import environment_setting # Django's default is 0: open a connection for each request and close it when # the response is sent. For a process that serves one request after another @@ -22,7 +22,7 @@ def is_web_process(): supervisor programs set, and which is what will answer it if the server ever becomes daphne or uvicorn. ''' - if os.environ.get('AWX_WEB_PROCESS'): + if environment_setting('WEB_PROCESS'): return True try: import uwsgi # noqa: F401 diff --git a/awx/settings/defaults.py b/awx/settings/defaults.py index 876878392..5ca1855b4 100644 --- a/awx/settings/defaults.py +++ b/awx/settings/defaults.py @@ -14,6 +14,7 @@ # python-ldap import ldap +from awx.settings.environment import environment_setting DEBUG = True SQL_DEBUG = DEBUG @@ -98,7 +99,7 @@ # the K8S cluster where awx itself is running) IS_K8S = False -AWX_CONTAINER_GROUP_K8S_API_TIMEOUT = 10 +ASCENDER_CONTAINER_GROUP_K8S_API_TIMEOUT = 10 # Whether container group Kubernetes API calls should be routed through the # HTTP_PROXY/HTTPS_PROXY environment variables of the task container. The Python # Kubernetes client started honouring those on its own in 34.1, but this traffic @@ -106,14 +107,14 @@ # verification against the cluster CA (the client verifies against the cluster # credential's CA data or the service account CA, never the system trust store). # Leave disabled unless the cluster API really is only reachable through a proxy. -AWX_CONTAINER_GROUP_K8S_API_USE_PROXY = False -AWX_CONTAINER_GROUP_DEFAULT_NAMESPACE = os.getenv('MY_POD_NAMESPACE', 'default') -AWX_CONTAINER_GROUP_DEFAULT_JOB_LABEL = os.getenv('AWX_CONTAINER_GROUP_DEFAULT_JOB_LABEL', 'ansible_job') +ASCENDER_CONTAINER_GROUP_K8S_API_USE_PROXY = False +ASCENDER_CONTAINER_GROUP_DEFAULT_NAMESPACE = os.getenv('MY_POD_NAMESPACE', 'default') +ASCENDER_CONTAINER_GROUP_DEFAULT_JOB_LABEL = os.getenv('ASCENDER_CONTAINER_GROUP_DEFAULT_JOB_LABEL', 'ansible_job') # Timeout when waiting for pod to enter running state. If the pod is still in pending state , it will be terminated. Valid time units are "s", "m", "h". Example : "5m" , "10s". -AWX_CONTAINER_GROUP_POD_PENDING_TIMEOUT = "2h" +ASCENDER_CONTAINER_GROUP_POD_PENDING_TIMEOUT = "2h" # How much capacity controlling a task costs a hybrid or control node -AWX_CONTROL_NODE_TASK_IMPACT = 1 +ASCENDER_CONTROL_NODE_TASK_IMPACT = 1 # Internationalization # https://docs.djangoproject.com/en/dev/topics/i18n/ @@ -337,7 +338,7 @@ # Without it the dispatcher worker running send_notifications is held for as # long as the far end keeps the connection open. Matches the default timeout of # the email backend. -AWX_NOTIFICATION_REQUEST_TIMEOUT = 30 +ASCENDER_NOTIFICATION_REQUEST_TIMEOUT = 30 # Time out task managers if they take longer than this many seconds, plus TASK_MANAGER_TIMEOUT_GRACE_PERIOD # We have the grace period so the task manager can bail out before the timeout. @@ -759,7 +760,7 @@ GALAXY_TASK_ENV = {'ANSIBLE_FORCE_COLOR': 'false', 'GIT_SSH_COMMAND': "ssh -o StrictHostKeyChecking=no"} # Rebuild Host Smart Inventory memberships. -AWX_REBUILD_SMART_MEMBERSHIP = False +ASCENDER_REBUILD_SMART_MEMBERSHIP = False # By default, allow arbitrary Jinja templating in extra_vars defined on a Job Template ALLOW_JINJA_IN_EXTRA_VARS = 'template' @@ -810,7 +811,7 @@ ASCENDER_ANSIBLE_CALLBACK_PLUGINS = "" # Automatically remove nodes that have missed their heartbeats after some time -AWX_AUTO_DEPROVISION_INSTANCES = False +ASCENDER_AUTO_DEPROVISION_INSTANCES = False # Enable Pendo on the UI, possible values are 'off', 'anonymous', and 'detailed' # Note: This setting may be overridden by database settings. @@ -1093,9 +1094,9 @@ } # If running on a VM, we log to files. When running in a container, we log to stdout. -logging_mode = os.getenv('AWX_LOGGING_MODE', 'file') +logging_mode = environment_setting('LOGGING_MODE', 'file') if logging_mode not in ('file', 'stdout'): - raise Exception("AWX_LOGGING_MODE must be 'file' or 'stdout'") + raise Exception("ASCENDER_LOGGING_MODE must be 'file' or 'stdout'") for name, config in handler_config.items(): # Common log handler config. Don't define a level here, it's set by settings.LOG_AGGREGATOR_LEVEL @@ -1133,20 +1134,20 @@ # ~ yum install graphviz # ~ dot -o profile.png -Tpng /var/log/tower/profile/some-profile-data.dot # -AWX_REQUEST_PROFILE_WITH_DOT = False +ASCENDER_REQUEST_PROFILE_WITH_DOT = False # Allow profiling callback workers via SIGUSR1 -AWX_CALLBACK_PROFILE = False +ASCENDER_CALLBACK_PROFILE = False # Delete temporary directories created to store playbook run-time ASCENDER_CLEANUP_PATHS = True # Allow ansible-runner to store env folder (may contain sensitive information) -AWX_RUNNER_OMIT_ENV_FILES = True +ASCENDER_RUNNER_OMIT_ENV_FILES = True # Allow ansible-runner to save ansible output # (changing to False may cause performance issues) -AWX_RUNNER_SUPPRESS_OUTPUT_FILE = True +ASCENDER_RUNNER_SUPPRESS_OUTPUT_FILE = True # https://github.com/ansible/ansible-runner/pull/1191/files # Interval in seconds between the last message and keep-alive messages that diff --git a/awx/settings/development.py b/awx/settings/development.py index e7db807bd..d63a6ab3d 100644 --- a/awx/settings/development.py +++ b/awx/settings/development.py @@ -55,7 +55,7 @@ CLUSTER_HOST_ID = socket.gethostname() -AWX_CALLBACK_PROFILE = True +ASCENDER_CALLBACK_PROFILE = True # ======================!!!!!!! FOR DEVELOPMENT ONLY !!!!!!!================================= # Disable normal scheduled/triggered task managers (DependencyManager, TaskManager, WorkflowManager). diff --git a/awx/settings/environment.py b/awx/settings/environment.py new file mode 100644 index 000000000..fb1b0a9a8 --- /dev/null +++ b/awx/settings/environment.py @@ -0,0 +1,32 @@ +# Copyright (c) 2026 Ascender +# All Rights Reserved. +"""Settings read from the environment, under either name. + +A handful of settings are read from the environment rather than from a settings +file, so the alias table in production.py cannot reach them: that table runs +after the settings modules have loaded, and two of these decide which settings +file loads at all. + +The Ascender name wins, the AWX one still answers, and the AWX one is what the +image, the installers and anyone's own start script pass today. They move when +it suits them rather than on the day of this change. +""" + +import os + +ASCENDER = 'ASCENDER_' +FORMER = 'AWX_' + + +def environment_setting(suffix, default=None): + """The `ASCENDER_` variable if set, else `AWX_`, else `default`. + + A name that is set but empty wins over the later one, matching what a plain + `os.environ.get(name, other)` chain did: exporting an empty value is how a + deployment says "not this", and treating it as unset would ignore that. + """ + for prefix in (ASCENDER, FORMER): + name = prefix + suffix + if name in os.environ: + return os.environ[name] + return default diff --git a/awx/settings/production.py b/awx/settings/production.py index 5e82e8d89..0fb605b2f 100644 --- a/awx/settings/production.py +++ b/awx/settings/production.py @@ -15,6 +15,7 @@ # Load default settings. from .defaults import * # NOQA +from awx.settings.environment import environment_setting DEBUG = False TEMPLATE_DEBUG = DEBUG @@ -56,12 +57,12 @@ # Load settings from any .py files in the global conf.d directory specified in # the environment, defaulting to /etc/tower/conf.d/. -settings_dir = os.environ.get('AWX_SETTINGS_DIR', '/etc/tower/conf.d/') +settings_dir = environment_setting('SETTINGS_DIR', '/etc/tower/conf.d/') settings_files = os.path.join(settings_dir, '*.py') # Load remaining settings from the global settings file specified in the # environment, defaulting to /etc/tower/settings.py. -settings_file = os.environ.get('AWX_SETTINGS_FILE', '/etc/tower/settings.py') +settings_file = environment_setting('SETTINGS_FILE', '/etc/tower/settings.py') # Attempt to load settings from /etc/tower/settings.py first, followed by # /etc/tower/conf.d/*.py. @@ -87,7 +88,7 @@ LOGGING = {} else: msg = 'No AWX configuration found at %s.' % settings_file - msg += '\nDefine the AWX_SETTINGS_FILE environment variable to ' + msg += '\nDefine the ASCENDER_SETTINGS_FILE environment variable to ' msg += 'specify an alternate path.' raise ImproperlyConfigured(msg) else: @@ -134,6 +135,19 @@ 'AWX_RUNNER_KEEPALIVE_SECONDS': 'ASCENDER_RUNNER_KEEPALIVE_SECONDS', 'AWX_SHOW_PLAYBOOK_LINKS': 'ASCENDER_SHOW_PLAYBOOK_LINKS', 'AWX_TASK_ENV': 'ASCENDER_TASK_ENV', + 'AWX_AUTO_DEPROVISION_INSTANCES': 'ASCENDER_AUTO_DEPROVISION_INSTANCES', + 'AWX_CALLBACK_PROFILE': 'ASCENDER_CALLBACK_PROFILE', + 'AWX_CONTAINER_GROUP_DEFAULT_JOB_LABEL': 'ASCENDER_CONTAINER_GROUP_DEFAULT_JOB_LABEL', + 'AWX_CONTAINER_GROUP_DEFAULT_NAMESPACE': 'ASCENDER_CONTAINER_GROUP_DEFAULT_NAMESPACE', + 'AWX_CONTAINER_GROUP_K8S_API_TIMEOUT': 'ASCENDER_CONTAINER_GROUP_K8S_API_TIMEOUT', + 'AWX_CONTAINER_GROUP_K8S_API_USE_PROXY': 'ASCENDER_CONTAINER_GROUP_K8S_API_USE_PROXY', + 'AWX_CONTAINER_GROUP_POD_PENDING_TIMEOUT': 'ASCENDER_CONTAINER_GROUP_POD_PENDING_TIMEOUT', + 'AWX_CONTROL_NODE_TASK_IMPACT': 'ASCENDER_CONTROL_NODE_TASK_IMPACT', + 'AWX_NOTIFICATION_REQUEST_TIMEOUT': 'ASCENDER_NOTIFICATION_REQUEST_TIMEOUT', + 'AWX_REBUILD_SMART_MEMBERSHIP': 'ASCENDER_REBUILD_SMART_MEMBERSHIP', + 'AWX_REQUEST_PROFILE_WITH_DOT': 'ASCENDER_REQUEST_PROFILE_WITH_DOT', + 'AWX_RUNNER_OMIT_ENV_FILES': 'ASCENDER_RUNNER_OMIT_ENV_FILES', + 'AWX_RUNNER_SUPPRESS_OUTPUT_FILE': 'ASCENDER_RUNNER_SUPPRESS_OUTPUT_FILE', } for _former, _current in _FORMER_NAMES.items(): _scope = locals() diff --git a/awx/settings/statement_timeout.py b/awx/settings/statement_timeout.py index 5f2070a06..9c4c9fb3b 100644 --- a/awx/settings/statement_timeout.py +++ b/awx/settings/statement_timeout.py @@ -1,4 +1,4 @@ -import os +from awx.settings.environment import environment_setting # What the uwsgi path used to work out to: harakiri of 115 seconds less a five # second margin. uvicorn serves the web process now and uwsgi is no longer @@ -39,7 +39,7 @@ def set_statement_timeout(DATABASES, DATABASE_STATEMENT_TIMEOUT=None): except (ImportError, ValueError, TypeError): pass - if timeout_ms is None and os.environ.get('AWX_WEB_PROCESS'): + if timeout_ms is None and environment_setting('WEB_PROCESS'): # a web process that is not uwsgi: same protection, explicit source timeout_ms = DATABASE_STATEMENT_TIMEOUT if DATABASE_STATEMENT_TIMEOUT is not None else DEFAULT_WEB_TIMEOUT_MS diff --git a/awx/settings/typed.py b/awx/settings/typed.py index 266617772..414d016d2 100644 --- a/awx/settings/typed.py +++ b/awx/settings/typed.py @@ -49,10 +49,18 @@ class AscenderSettings(Protocol): API_400_ERROR_LOG_FORMAT: str APPEND_SLASH: bool ASCENDER_ANSIBLE_CALLBACK_PLUGINS: list[Any] + ASCENDER_AUTO_DEPROVISION_INSTANCES: bool ASCENDER_AUTO_STATS_ENABLED: bool ASCENDER_AUTO_STATS_MAX_HOSTS: int + ASCENDER_CALLBACK_PROFILE: bool ASCENDER_CLEANUP_PATHS: bool ASCENDER_COLLECTIONS_ENABLED: bool + ASCENDER_CONTAINER_GROUP_DEFAULT_JOB_LABEL: str + ASCENDER_CONTAINER_GROUP_DEFAULT_NAMESPACE: str + ASCENDER_CONTAINER_GROUP_K8S_API_TIMEOUT: int + ASCENDER_CONTAINER_GROUP_K8S_API_USE_PROXY: bool + ASCENDER_CONTAINER_GROUP_POD_PENDING_TIMEOUT: str + ASCENDER_CONTROL_NODE_TASK_IMPACT: int ASCENDER_ENABLED_VALUE: str ASCENDER_ENABLED_VAR: str ASCENDER_EXCLUDE_EMPTY_GROUPS: bool @@ -60,9 +68,14 @@ class AscenderSettings(Protocol): ASCENDER_ISOLATION_BASE_PATH: str ASCENDER_ISOLATION_SHOW_PATHS: list[Any] ASCENDER_MOUNT_ISOLATED_PATHS_ON_K8S: bool + ASCENDER_NOTIFICATION_REQUEST_TIMEOUT: int + ASCENDER_REBUILD_SMART_MEMBERSHIP: bool ASCENDER_REQUEST_PROFILE: bool + ASCENDER_REQUEST_PROFILE_WITH_DOT: bool ASCENDER_ROLES_ENABLED: bool ASCENDER_RUNNER_KEEPALIVE_SECONDS: int + ASCENDER_RUNNER_OMIT_ENV_FILES: bool + ASCENDER_RUNNER_SUPPRESS_OUTPUT_FILE: bool ASCENDER_SHOW_PLAYBOOK_LINKS: bool ASCENDER_TASK_ENV: dict[Any, Any] ASCENDER_URL_BASE: str @@ -171,20 +184,7 @@ class AscenderSettings(Protocol): AUTOMATION_ANALYTICS_LAST_ENTRIES: str AUTOMATION_ANALYTICS_LAST_GATHER: Any AUTOMATION_ANALYTICS_URL: str - AWX_AUTO_DEPROVISION_INSTANCES: bool - AWX_CALLBACK_PROFILE: bool - AWX_CONTAINER_GROUP_DEFAULT_JOB_LABEL: str - AWX_CONTAINER_GROUP_DEFAULT_NAMESPACE: str - AWX_CONTAINER_GROUP_K8S_API_TIMEOUT: int - AWX_CONTAINER_GROUP_K8S_API_USE_PROXY: bool - AWX_CONTAINER_GROUP_POD_PENDING_TIMEOUT: str - AWX_CONTROL_NODE_TASK_IMPACT: int AWX_DISABLE_TASK_MANAGERS: bool - AWX_NOTIFICATION_REQUEST_TIMEOUT: int - AWX_REBUILD_SMART_MEMBERSHIP: bool - AWX_REQUEST_PROFILE_WITH_DOT: bool - AWX_RUNNER_OMIT_ENV_FILES: bool - AWX_RUNNER_SUPPRESS_OUTPUT_FILE: bool AZURE_RM_ENABLED_VALUE: str AZURE_RM_ENABLED_VAR: str AZURE_RM_EXCLUDE_EMPTY_GROUPS: bool diff --git a/docs/capacity.md b/docs/capacity.md index b50431c3b..4d48c79c5 100644 --- a/docs/capacity.md +++ b/docs/capacity.md @@ -92,15 +92,15 @@ Other job types have a fixed execution impact: * Project Updates: 1 * System Jobs: 5 -For jobs that execute on the same node as they are controlled by, both settings.AWX_CONTROL_NODE_TASK_IMPACT and the job task execution impact apply. +For jobs that execute on the same node as they are controlled by, both settings.ASCENDER_CONTROL_NODE_TASK_IMPACT and the job task execution impact apply. Examples: -Given settings.AWX_CONTROL_NODE_TASK_IMPACT is 1: +Given settings.ASCENDER_CONTROL_NODE_TASK_IMPACT is 1: - Project updates (where the execution_node is always the same as the controller_node), have a total impact of 2. - Container group jobs (where the execution node is not a member of the cluster) only control impact applies, and the controller node has a total task impact of 1. - A job executing on a "hybrid" node where both control and execution will occur on the same node has the task impact of (1 overhead for ansible main process) + (min(forks,hosts)) + (1 control node task impact). Meaning a Job running on a hybrid node with forks set to 1 would have a total task impact of 3. -### Selecting the Right settings.AWX_CONTROL_NODE_TASK_IMPACT +### Selecting the Right settings.ASCENDER_CONTROL_NODE_TASK_IMPACT This setting allows you to determine how much impact controlling jobs has. This can be helpful if you notice symptoms of your control plane exceeding desired diff --git a/docs/deprecated/inventory_refresh.md b/docs/deprecated/inventory_refresh.md index 9c021ab1d..de2e27656 100644 --- a/docs/deprecated/inventory_refresh.md +++ b/docs/deprecated/inventory_refresh.md @@ -52,7 +52,7 @@ are generated by the `update_host_smart_inventory_memberships` task. The task is * Existing Host is changed (update/delete). * New Smart Inventory is added. * Existing Smart Inventory is changed (update/delete). - * **NOTE:** This task is only run if the `AWX_REBUILD_SMART_MEMBERSHIP` is set to `True`. It defaults to `False`. + * **NOTE:** This task is only run if the `ASCENDER_REBUILD_SMART_MEMBERSHIP` is set to `True`. It defaults to `False`. ### Smart Filter (`host_filter`) The `SmartFilter` class handles our translation of the smart search string. We store the diff --git a/docs/docsite/rst/userguide/inventories.rst b/docs/docsite/rst/userguide/inventories.rst index 219030e4d..d367ff2b8 100644 --- a/docs/docsite/rst/userguide/inventories.rst +++ b/docs/docsite/rst/userguide/inventories.rst @@ -72,7 +72,7 @@ The ``host`` model has a related endpoint, ``smart_inventories`` that identifies .. note:: - To update the memberships more frequently, you can change the file-based setting ``AWX_REBUILD_SMART_MEMBERSHIP`` to **True** (default is False). This will update memberships in the following events: + To update the memberships more frequently, you can change the file-based setting ``ASCENDER_REBUILD_SMART_MEMBERSHIP`` to **True** (default is False). This will update memberships in the following events: - a new host is added - an existing host is modified (updated or deleted) diff --git a/docs/tasks.md b/docs/tasks.md index cd2c33627..16caae852 100644 --- a/docs/tasks.md +++ b/docs/tasks.md @@ -245,7 +245,7 @@ Running the Update Inventory Computed Fields task in the background, in response The `smart_inventories` field in Ascender uses a membership lookup table that identifies the set of every Smart Inventory a host is associated with. This particular task generates memberships and is launched whenever certain conditions are met (_e.g._, a new host is added or an existing host is modified). -An important thing to note is that this task is only run if the `AWX_REBUILD_SMART_MEMBERSHIP` is set to `True` (default is `False`). +An important thing to note is that this task is only run if the `ASCENDER_REBUILD_SMART_MEMBERSHIP` is set to `True` (default is `False`). For more information, visit the [Smart Inventories section](https://docs.ansible.com/ansible-tower/latest/html/userguide/inventories.html#smart-inventories) of the Tower User Guide's "Inventory" page or the Ascender documentation page [Inventory Refresh Overview page](github.com/ctrliq/ascender/blob/main/docs/inventory_refresh.md#inventory-changes) in this repo. From b9c3c663ab8b89333f3b3a3e4fe1eabfc8690069 Mon Sep 17 00:00:00 2001 From: Blai Peidro Date: Tue, 15 Sep 2026 20:49:51 +0200 Subject: [PATCH 2/2] refactor: import environment_setting from ascender rather than awx #997 landed while this branch was open, so the module this adds now lives at ascender/settings/environment.py and the five imports of it named the old package. They resolved, because the awx shim aliases the whole package, but a settings module that cannot load without the compatibility layer is the wrong dependency to take on: production.py and defaults.py are imported to decide which settings file loads at all, and this is the branch that gives those settings the Ascender name in the first place. --- ascender/main/tests/unit/settings/test_environment_names.py | 2 +- ascender/settings/connection_reuse.py | 2 +- ascender/settings/defaults.py | 2 +- ascender/settings/production.py | 2 +- ascender/settings/statement_timeout.py | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/ascender/main/tests/unit/settings/test_environment_names.py b/ascender/main/tests/unit/settings/test_environment_names.py index 5b0e04535..4d3e12891 100644 --- a/ascender/main/tests/unit/settings/test_environment_names.py +++ b/ascender/main/tests/unit/settings/test_environment_names.py @@ -13,7 +13,7 @@ import pytest -from awx.settings.environment import environment_setting +from ascender.settings.environment import environment_setting @pytest.fixture diff --git a/ascender/settings/connection_reuse.py b/ascender/settings/connection_reuse.py index b7043d7d8..c6dc85886 100644 --- a/ascender/settings/connection_reuse.py +++ b/ascender/settings/connection_reuse.py @@ -1,4 +1,4 @@ -from awx.settings.environment import environment_setting +from ascender.settings.environment import environment_setting # Django's default is 0: open a connection for each request and close it when # the response is sent. For a process that serves one request after another diff --git a/ascender/settings/defaults.py b/ascender/settings/defaults.py index 81e87e2e0..43088ee1f 100644 --- a/ascender/settings/defaults.py +++ b/ascender/settings/defaults.py @@ -14,7 +14,7 @@ # python-ldap import ldap -from awx.settings.environment import environment_setting +from ascender.settings.environment import environment_setting DEBUG = True SQL_DEBUG = DEBUG diff --git a/ascender/settings/production.py b/ascender/settings/production.py index d8b53f96c..ebba78e2b 100644 --- a/ascender/settings/production.py +++ b/ascender/settings/production.py @@ -15,7 +15,7 @@ # Load default settings. from .defaults import * # NOQA -from awx.settings.environment import environment_setting +from ascender.settings.environment import environment_setting DEBUG = False TEMPLATE_DEBUG = DEBUG diff --git a/ascender/settings/statement_timeout.py b/ascender/settings/statement_timeout.py index 9c4c9fb3b..3e8942316 100644 --- a/ascender/settings/statement_timeout.py +++ b/ascender/settings/statement_timeout.py @@ -1,4 +1,4 @@ -from awx.settings.environment import environment_setting +from ascender.settings.environment import environment_setting # What the uwsgi path used to work out to: harakiri of 115 seconds less a five # second margin. uvicorn serves the web process now and uwsgi is no longer