diff --git a/.coveragerc b/.coveragerc index 086c99d2c80e..caafe9b6df90 100644 --- a/.coveragerc +++ b/.coveragerc @@ -19,6 +19,8 @@ exclude_also = branch = True omit = awx/main/migrations/* + awx/settings/defaults.py + awx/settings/*_defaults.py source = . source_pkgs = diff --git a/awx/__init__.py b/awx/__init__.py index 6b2f809c3027..59cccd5d8b87 100644 --- a/awx/__init__.py +++ b/awx/__init__.py @@ -62,7 +62,8 @@ def version_file(): def prepare_env(): # Update the default settings environment variable based on current mode. - os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'awx.settings.%s' % MODE) + os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'awx.settings') + os.environ.setdefault('AWX_MODE', MODE) # Hide DeprecationWarnings when running in production. Need to first load # settings to apply our filter after Django's own warnings filter. from django.conf import settings diff --git a/awx/api/generics.py b/awx/api/generics.py index 71c42f6b4d84..207799b27d06 100644 --- a/awx/api/generics.py +++ b/awx/api/generics.py @@ -161,7 +161,7 @@ def get_view_description(view, html=False): def get_default_schema(): - if settings.SETTINGS_MODULE == 'awx.settings.development': + if settings.DYNACONF.is_development_mode: from awx.api.swagger import schema_view return schema_view diff --git a/awx/main/tests/unit/test_settings.py b/awx/main/tests/unit/test_settings.py index 7ff2e3f4abf6..bd18ee9d95e6 100644 --- a/awx/main/tests/unit/test_settings.py +++ b/awx/main/tests/unit/test_settings.py @@ -1,6 +1,3 @@ -from split_settings.tools import include - - LOCAL_SETTINGS = ( 'ALLOWED_HOSTS', 'BROADCAST_WEBSOCKET_PORT', @@ -16,13 +13,14 @@ def test_postprocess_auth_basic_enabled(): - locals().update({'__file__': __file__}) + """The final loaded settings should have basic auth enabled.""" + from awx.settings import REST_FRAMEWORK - include('../../../settings/defaults.py', scope=locals()) - assert 'awx.api.authentication.LoggedBasicAuthentication' in locals()['REST_FRAMEWORK']['DEFAULT_AUTHENTICATION_CLASSES'] + assert 'awx.api.authentication.LoggedBasicAuthentication' in REST_FRAMEWORK['DEFAULT_AUTHENTICATION_CLASSES'] def test_default_settings(): + """Ensure that all default settings are present in the snapshot.""" from django.conf import settings for k in dir(settings): @@ -31,3 +29,64 @@ def test_default_settings(): default_val = getattr(settings.default_settings, k, None) snapshot_val = settings.DEFAULTS_SNAPSHOT[k] assert default_val == snapshot_val, f'Setting for {k} does not match shapshot:\nsnapshot: {snapshot_val}\ndefault: {default_val}' + + +def test_django_conf_settings_is_awx_settings(): + """Ensure that the settings loaded from dynaconf are the same as the settings delivered to django.""" + from django.conf import settings + from awx.settings import REST_FRAMEWORK + + assert settings.REST_FRAMEWORK == REST_FRAMEWORK + + +def test_dynaconf_is_awx_settings(): + """Ensure that the settings loaded from dynaconf are the same as the settings delivered to django.""" + from django.conf import settings + from awx.settings import REST_FRAMEWORK + + assert settings.DYNACONF.REST_FRAMEWORK == REST_FRAMEWORK + + +def test_development_settings_can_be_directly_imported(monkeypatch): + """Ensure that the development settings can be directly imported.""" + monkeypatch.setenv('AWX_MODE', 'development') + from django.conf import settings + from awx.settings.development import REST_FRAMEWORK + from awx.settings.development import DEBUG # actually set on defaults.py and not overridden in development.py + + assert settings.REST_FRAMEWORK == REST_FRAMEWORK + assert DEBUG is True + + +def test_toggle_feature_flags(): + """Ensure that the toggle_feature_flags function works as expected.""" + from awx.settings.functions import toggle_feature_flags + + settings = { + "FLAGS": { + "FEATURE_SOME_PLATFORM_FLAG_ENABLED": [ + {"condition": "boolean", "value": False, "required": True}, + {"condition": "before date", "value": "2022-06-01T12:00Z"}, + ] + }, + "FEATURE_SOME_PLATFORM_FLAG_ENABLED": True, + } + assert toggle_feature_flags(settings) == { + "FLAGS__FEATURE_SOME_PLATFORM_FLAG_ENABLED": [ + {"condition": "boolean", "value": True, "required": True}, + {"condition": "before date", "value": "2022-06-01T12:00Z"}, + ] + } + + +def test_merge_application_name(): + """Ensure that the merge_application_name function works as expected.""" + from awx.settings.functions import merge_application_name + + settings = { + "DATABASES__default__ENGINE": "django.db.backends.postgresql", + "CLUSTER_HOST_ID": "test-cluster-host-id", + } + result = merge_application_name(settings)["DATABASES__default__OPTIONS__application_name"] + assert result.startswith("awx-") + assert "test-cluster" in result diff --git a/awx/settings/__init__.py b/awx/settings/__init__.py index e484e62be15d..0b2739bde1a4 100644 --- a/awx/settings/__init__.py +++ b/awx/settings/__init__.py @@ -1,2 +1,109 @@ # Copyright (c) 2015 Ansible, Inc. # All Rights Reserved. +import os +from ansible_base.lib.dynamic_config import ( + factory, + export, + load_envvars, + load_python_file_with__injected_context, + load_standard_settings_files, + validate, +) +from .functions import merge_application_name, toggle_feature_flags, add_backwards_compatibility + + +add_backwards_compatibility() + + +# Create a the standard DYNACONF instance which will come with DAB defaults +# This loads defaults.py and environment specific file e.g: development_defaults.py +DYNACONF = factory( + __name__, + "AWX", + environments=("development", "production", "quiet", "kube"), + settings_files=["defaults.py"], +) + +# Store snapshot before loading any custom config file +DEFAULTS_SNAPSHOT = {} +if DYNACONF.is_development_mode: + DYNACONF.set( + "DEFAULTS_SNAPSHOT", + DYNACONF.as_dict(internal=False), # should use deepcopy here? + loader_identifier="awx.settings:DEFAULTS_SNAPSHOT", + ) +########################################################################################## +# Any settings loaded after this point will be marked as as a read_only database setting +########################################################################################## + +# Load extra config from the now deprecated /etc/tower/ directory +# This is only for backwards compatibility and will be removed in the future +# Load settings from any .py files in the global conf.d directory specified in +# the environment, defaulting to /etc/tower/conf.d/. +# Load remaining settings from the global settings file specified in the +# environment, defaulting to /etc/tower/settings.py. +# Attempt to load settings from /etc/tower/settings.py first, followed by +# /etc/tower/conf.d/*.py. +settings_dir = os.environ.get('AWX_SETTINGS_DIR', '/etc/tower/conf.d/') +settings_files_path = os.path.join(settings_dir, '*.py') +settings_file_path = os.environ.get('AWX_SETTINGS_FILE', '/etc/tower/settings.py') + +# The following should have been loaded using `DYNACONF.load_file` but we are +# using `load_python_file_with__injected_context` to maintain backwards compatibility +load_python_file_with__injected_context(settings_files_path, settings=DYNACONF) +load_python_file_with__injected_context(settings_file_path, settings=DYNACONF) + + +# Load new standard settings files from +# /etc/ansible-automation-platform/ and /etc/ansible-automation-platform/awx/ +# this is to allow for a smoother transition from legacy (above) to new settings standard paths +load_standard_settings_files(DYNACONF) + +# Load optional development only settings +if DYNACONF.get_environ("AWX_KUBE_DEVEL"): + load_python_file_with__injected_context("kube_defaults.py", settings=DYNACONF) +else: + load_python_file_with__injected_context("local_*.py", settings=DYNACONF) + +# Check at least one required setting file has been loaded +if "production" in DYNACONF.current_env.lower(): # pragma: no cover + required_settings_paths = [ + os.path.dirname(settings_file_path), + "/etc/ansible-automation-platform/", + ] + # check if at least one file has been loaded any of the required paths + # use DYNACONF._loaded_files to check any filename inside the paths + # if not loaded then raise an ImproperlyConfigured error + for path in required_settings_paths: + if any(path in f for f in DYNACONF._loaded_files): + break + else: + from django.core.exceptions import ImproperlyConfigured + + msg = 'No AWX configuration found at %s.' % required_settings_paths + msg += '\nDefine the AWX_SETTINGS_FILE environment variable to ' + msg += 'specify an alternate path.' + raise ImproperlyConfigured(msg) + +# Load envvars at the end to allow them to override everything loaded so far +load_envvars(DYNACONF) + +# This must run after all custom settings are imported +DYNACONF.update( + merge_application_name(DYNACONF), + loader_identifier="awx.settings:merge_application_name", + merge=True, +) + +# Toggle feature flags based on installer settings +DYNACONF.update( + toggle_feature_flags(DYNACONF), + loader_identifier="awx.settings:toggle_feature_flags", + merge=True, +) + +# Update django.conf.settings with DYNACONF keys. +export(__name__, DYNACONF) + +# Validate the settings according to the validators registered +validate(DYNACONF) diff --git a/awx/settings/application_name.py b/awx/settings/application_name.py index ed76886c395e..ac7e40553e00 100644 --- a/awx/settings/application_name.py +++ b/awx/settings/application_name.py @@ -25,6 +25,7 @@ def get_application_name(CLUSTER_HOST_ID, function=''): def set_application_name(DATABASES, CLUSTER_HOST_ID, function=''): + """In place modification of DATABASES to set the application name for the connection.""" # If settings files were not properly passed DATABASES could be {} at which point we don't need to set the app name. if not DATABASES or 'default' not in DATABASES: return diff --git a/awx/settings/defaults.py b/awx/settings/defaults.py index 2f83414a482e..6767448cc08b 100644 --- a/awx/settings/defaults.py +++ b/awx/settings/defaults.py @@ -9,9 +9,6 @@ import socket from datetime import timedelta -from split_settings.tools import include - - DEBUG = True SQL_DEBUG = DEBUG @@ -1011,17 +1008,12 @@ } } - # django-ansible-base ANSIBLE_BASE_TEAM_MODEL = 'main.Team' ANSIBLE_BASE_ORGANIZATION_MODEL = 'main.Organization' ANSIBLE_BASE_RESOURCE_CONFIG_MODULE = 'awx.resource_api' ANSIBLE_BASE_PERMISSION_MODEL = 'main.Permission' -from ansible_base.lib import dynamic_config # noqa: E402 - -include(os.path.join(os.path.dirname(dynamic_config.__file__), 'dynamic_settings.py')) - # Add a postfix to the API URL patterns # example if set to '' API pattern will be /api # example if set to 'controller' API pattern will be /api AND /api/controller diff --git a/awx/settings/development.py b/awx/settings/development.py index d38c2759e2c0..5b630c49841a 100644 --- a/awx/settings/development.py +++ b/awx/settings/development.py @@ -1,126 +1,13 @@ -# Copyright (c) 2015 Ansible, Inc. -# All Rights Reserved. - -# Development settings for AWX project. - -# Python +# This file exists for backwards compatibility only +# the current way of running AWX is to point settings to +# awx/settings/__init__.py as the entry point for the settings +# that is done by exporting: export DJANGO_SETTINGS_MODULE=awx.settings import os -import socket -import copy -import sys -import traceback - -# Centos-7 doesn't include the svg mime type -# /usr/lib64/python/mimetypes.py -import mimetypes - -# Django Split Settings -from split_settings.tools import optional, include - -# Load default settings. -from .defaults import * # NOQA - -# awx-manage shell_plus --notebook -NOTEBOOK_ARGUMENTS = ['--NotebookApp.token=', '--ip', '0.0.0.0', '--port', '9888', '--allow-root', '--no-browser'] - -# print SQL queries in shell_plus -SHELL_PLUS_PRINT_SQL = False - -# show colored logs in the dev environment -# to disable this, set `COLOR_LOGS = False` in awx/settings/local_settings.py -COLOR_LOGS = True -LOGGING['handlers']['console']['()'] = 'awx.main.utils.handlers.ColorHandler' # noqa - -ALLOWED_HOSTS = ['*'] - -mimetypes.add_type("image/svg+xml", ".svg", True) -mimetypes.add_type("image/svg+xml", ".svgz", True) - -# Disallow sending session cookies over insecure connections -SESSION_COOKIE_SECURE = False - -# Disallow sending csrf cookies over insecure connections -CSRF_COOKIE_SECURE = False - -# Disable Pendo on the UI for development/test. -# Note: This setting may be overridden by database settings. -PENDO_TRACKING_STATE = "off" -INSIGHTS_TRACKING_STATE = False - -# debug toolbar and swagger assume that requirements/requirements_dev.txt are installed - -INSTALLED_APPS += ['drf_yasg', 'debug_toolbar'] # NOQA - -MIDDLEWARE = ['debug_toolbar.middleware.DebugToolbarMiddleware'] + MIDDLEWARE # NOQA - -DEBUG_TOOLBAR_CONFIG = {'ENABLE_STACKTRACES': True} - -# Configure a default UUID for development only. -SYSTEM_UUID = '00000000-0000-0000-0000-000000000000' -INSTALL_UUID = '00000000-0000-0000-0000-000000000000' - -# Ansible base virtualenv paths and enablement -# only used for deprecated fields and management commands for them -BASE_VENV_PATH = os.path.realpath("/var/lib/awx/venv") - -CLUSTER_HOST_ID = socket.gethostname() - -AWX_CALLBACK_PROFILE = True - -# ======================!!!!!!! FOR DEVELOPMENT ONLY !!!!!!!================================= -# Disable normal scheduled/triggered task managers (DependencyManager, TaskManager, WorkflowManager). -# Allows user to trigger task managers directly for debugging and profiling purposes. -# Only works in combination with settings.SETTINGS_MODULE == 'awx.settings.development' -AWX_DISABLE_TASK_MANAGERS = False - -# Needed for launching runserver in debug mode -# ======================!!!!!!! FOR DEVELOPMENT ONLY !!!!!!!================================= - -# Store a snapshot of default settings at this point before loading any -# customizable config files. -this_module = sys.modules[__name__] -local_vars = dir(this_module) -DEFAULTS_SNAPSHOT = {} # define after we save local_vars so we do not snapshot the snapshot -for setting in local_vars: - if setting.isupper(): - DEFAULTS_SNAPSHOT[setting] = copy.deepcopy(getattr(this_module, setting)) - -del local_vars # avoid temporary variables from showing up in dir(settings) -del this_module -# -############################################################################################### -# -# Any settings defined after this point will be marked as as a read_only database setting -# -################################################################################################ - -# If there is an `/etc/tower/settings.py`, include it. -# If there is a `/etc/tower/conf.d/*.py`, include them. -include(optional('/etc/tower/settings.py'), scope=locals()) -include(optional('/etc/tower/conf.d/*.py'), scope=locals()) - -# If any local_*.py files are present in awx/settings/, use them to override -# default settings for development. If not present, we can still run using -# only the defaults. -# this needs to stay at the bottom of this file -try: - if os.getenv('AWX_KUBE_DEVEL', False): - include(optional('development_kube.py'), scope=locals()) - else: - include(optional('local_*.py'), scope=locals()) -except ImportError: - traceback.print_exc() - sys.exit(1) - -# The below runs AFTER all of the custom settings are imported -# because conf.d files will define DATABASES and this should modify that -from .application_name import set_application_name -set_application_name(DATABASES, CLUSTER_HOST_ID) # NOQA +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "awx.settings") +os.environ.setdefault("AWX_MODE", "development") -del set_application_name +from ansible_base.lib.dynamic_config import export +from . import DYNACONF # noqa -# Set the value of any feature flags that are defined in the local settings -for feature in list(FLAGS.keys()): # noqa: F405 - if feature in locals(): - FLAGS[feature][0]['value'] = locals()[feature] # noqa: F405 +export(__name__, DYNACONF) diff --git a/awx/settings/development_defaults.py b/awx/settings/development_defaults.py new file mode 100644 index 000000000000..2e4732cebcb5 --- /dev/null +++ b/awx/settings/development_defaults.py @@ -0,0 +1,66 @@ +# Copyright (c) 2015 Ansible, Inc. +# All Rights Reserved. + +# Development settings for AWX project. + +# Python +import os +import socket + +# Centos-7 doesn't include the svg mime type +# /usr/lib64/python/mimetypes.py +import mimetypes + +# awx-manage shell_plus --notebook +NOTEBOOK_ARGUMENTS = ['--NotebookApp.token=', '--ip', '0.0.0.0', '--port', '9888', '--allow-root', '--no-browser'] + +# print SQL queries in shell_plus +SHELL_PLUS_PRINT_SQL = False + +# show colored logs in the dev environment +# to disable this, set `COLOR_LOGS = False` in awx/settings/local_settings.py +COLOR_LOGS = True +LOGGING__handlers__console = '@merge {"()": "awx.main.utils.handlers.ColorHandler"}' + +ALLOWED_HOSTS = ['*'] + +mimetypes.add_type("image/svg+xml", ".svg", True) +mimetypes.add_type("image/svg+xml", ".svgz", True) + +# Disallow sending session cookies over insecure connections +SESSION_COOKIE_SECURE = False + +# Disallow sending csrf cookies over insecure connections +CSRF_COOKIE_SECURE = False + +# Disable Pendo on the UI for development/test. +# Note: This setting may be overridden by database settings. +PENDO_TRACKING_STATE = "off" +INSIGHTS_TRACKING_STATE = False + +# debug toolbar and swagger assume that requirements/requirements_dev.txt are installed +INSTALLED_APPS = "@merge drf_yasg,debug_toolbar" +MIDDLEWARE = "@insert 0 debug_toolbar.middleware.DebugToolbarMiddleware" + +DEBUG_TOOLBAR_CONFIG = {'ENABLE_STACKTRACES': True} + +# Configure a default UUID for development only. +SYSTEM_UUID = '00000000-0000-0000-0000-000000000000' +INSTALL_UUID = '00000000-0000-0000-0000-000000000000' + +# Ansible base virtualenv paths and enablement +# only used for deprecated fields and management commands for them +BASE_VENV_PATH = os.path.realpath("/var/lib/awx/venv") + +CLUSTER_HOST_ID = socket.gethostname() + +AWX_CALLBACK_PROFILE = True + +# ======================!!!!!!! FOR DEVELOPMENT ONLY !!!!!!!================================= +# Disable normal scheduled/triggered task managers (DependencyManager, TaskManager, WorkflowManager). +# Allows user to trigger task managers directly for debugging and profiling purposes. +# Only works in combination with settings.SETTINGS_MODULE == 'awx.settings.development' +AWX_DISABLE_TASK_MANAGERS = False + +# Needed for launching runserver in debug mode +# ======================!!!!!!! FOR DEVELOPMENT ONLY !!!!!!!================================= diff --git a/awx/settings/development_kube.py b/awx/settings/development_kube.py index c30a7fe025fe..e6ba6170c6d7 100644 --- a/awx/settings/development_kube.py +++ b/awx/settings/development_kube.py @@ -1,4 +1,13 @@ -BROADCAST_WEBSOCKET_SECRET = '🤖starscream🤖' -BROADCAST_WEBSOCKET_PORT = 8052 -BROADCAST_WEBSOCKET_VERIFY_CERT = False -BROADCAST_WEBSOCKET_PROTOCOL = 'http' +# This file exists for backwards compatibility only +# the current way of running AWX is to point settings to +# awx/settings/__init__.py as the entry point for the settings +# that is done by exporting: export DJANGO_SETTINGS_MODULE=awx.settings +import os + +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "awx.settings") +os.environ.setdefault("AWX_MODE", "development,kube") + +from ansible_base.lib.dynamic_config import export +from . import DYNACONF # noqa + +export(__name__, DYNACONF) diff --git a/awx/settings/development_quiet.py b/awx/settings/development_quiet.py index c47e78b69d86..5fea2756e908 100644 --- a/awx/settings/development_quiet.py +++ b/awx/settings/development_quiet.py @@ -1,15 +1,13 @@ -# Copyright (c) 2015 Ansible, Inc. -# All Rights Reserved. +# This file exists for backwards compatibility only +# the current way of running AWX is to point settings to +# awx/settings/__init__.py as the entry point for the settings +# that is done by exporting: export DJANGO_SETTINGS_MODULE=awx.settings +import os -# Development settings for AWX project, but with DEBUG disabled +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "awx.settings") +os.environ.setdefault("AWX_MODE", "development,quiet") -# Load development settings. -from defaults import * # NOQA +from ansible_base.lib.dynamic_config import export +from . import DYNACONF # noqa -# Load development settings. -from development import * # NOQA - -# Disable capturing DEBUG -DEBUG = False -TEMPLATE_DEBUG = DEBUG -SQL_DEBUG = DEBUG +export(__name__, DYNACONF) diff --git a/awx/settings/functions.py b/awx/settings/functions.py new file mode 100644 index 000000000000..0ac643739641 --- /dev/null +++ b/awx/settings/functions.py @@ -0,0 +1,70 @@ +import os +from typing import Any +from dynaconf import Dynaconf +from dynaconf.utils.functional import empty +from .application_name import get_application_name + + +def toggle_feature_flags(settings: Dynaconf) -> dict[str, Any]: + """Toggle FLAGS based on installer settings. + FLAGS is a django-flags formatted dictionary. + FLAGS={ + "FEATURE_SOME_PLATFORM_FLAG_ENABLED": [ + {"condition": "boolean", "value": False, "required": True}, + {"condition": "before date", "value": "2022-06-01T12:00Z"}, + ] + } + Installers will place `FEATURE_SOME_PLATFORM_FLAG_ENABLED=True/False` in the settings file. + This function will update the value in the index 0 in FLAGS with the installer value. + """ + data = {} + for feature_name, feature_content in settings.get("FLAGS", {}).items(): + if (installer_value := settings.get(feature_name, empty)) is not empty: + feature_content[0]["value"] = installer_value + data[f"FLAGS__{feature_name}"] = feature_content + return data + + +def merge_application_name(settings): + """Return a dynaconf merge dict to set the application name for the connection.""" + data = {} + if "sqlite3" not in settings.get("DATABASES__default__ENGINE", ""): + data["DATABASES__default__OPTIONS__application_name"] = get_application_name(settings.get("CLUSTER_HOST_ID")) + return data + + +def add_backwards_compatibility(): + """Add backwards compatibility for AWX_MODE. + + Before dynaconf integration the usage of AWX settings was supported to be just + DJANGO_SETTINGS_MODULE=awx.settings.production or DJANGO_SETTINGS_MODULE=awx.settings.development + (development_quiet and development_kube were also supported). + + With dynaconf the DJANGO_SETTINGS_MODULE should be set always to "awx.settings" as the only entry point + for settings and then "AWX_MODE" can be set to any of production,development,quiet,kube + or a combination of them separated by comma. + + E.g: + + export DJANGO_SETTINGS_MODULE=awx.settings + export AWX_MODE=production + awx-manage [command] + dynaconf [command] + + If pointing `DJANGO_SETTINGS_MODULE` to `awx.settings.production` or `awx.settings.development` then + this function will set `AWX_MODE` to the correct value. + """ + django_settings_module = os.getenv("DJANGO_SETTINGS_MODULE", "awx.settings") + if django_settings_module == "awx.settings": + return + + current_mode = os.getenv("AWX_MODE", "") + for _module_name in ["development", "production", "development_quiet", "development_kube"]: + if django_settings_module == f"awx.settings.{_module_name}": + _mode = current_mode.split(",") + if "development_" in _module_name and "development" not in current_mode: + _mode.append("development") + _mode_fragment = _module_name.replace("development_", "") + if _mode_fragment not in _mode: + _mode.append(_mode_fragment) + os.environ["AWX_MODE"] = ",".join(_mode) diff --git a/awx/settings/kube_defaults.py b/awx/settings/kube_defaults.py new file mode 100644 index 000000000000..c30a7fe025fe --- /dev/null +++ b/awx/settings/kube_defaults.py @@ -0,0 +1,4 @@ +BROADCAST_WEBSOCKET_SECRET = '🤖starscream🤖' +BROADCAST_WEBSOCKET_PORT = 8052 +BROADCAST_WEBSOCKET_VERIFY_CERT = False +BROADCAST_WEBSOCKET_PROTOCOL = 'http' diff --git a/awx/settings/production.py b/awx/settings/production.py index e340de4fbbc1..bcf483b118cf 100644 --- a/awx/settings/production.py +++ b/awx/settings/production.py @@ -1,111 +1,13 @@ -# Copyright (c) 2015 Ansible, Inc. -# All Rights Reserved. - -# Production settings for AWX project. - -# Python +# This file exists for backwards compatibility only +# the current way of running AWX is to point settings to +# awx/settings/__init__.py as the entry point for the settings +# that is done by exporting: export DJANGO_SETTINGS_MODULE=awx.settings import os -import copy -import errno -import sys -import traceback - -# Django Split Settings -from split_settings.tools import optional, include - -# Load default settings. -from .defaults import * # NOQA - -DEBUG = False -TEMPLATE_DEBUG = DEBUG -SQL_DEBUG = DEBUG - -# Clear database settings to force production environment to define them. -DATABASES = {} - -# Clear the secret key to force production environment to define it. -SECRET_KEY = None - -# Hosts/domain names that are valid for this site; required if DEBUG is False -# See https://docs.djangoproject.com/en/dev/ref/settings/#allowed-hosts -ALLOWED_HOSTS = [] - -# Ansible base virtualenv paths and enablement -# only used for deprecated fields and management commands for them -BASE_VENV_PATH = os.path.realpath("/var/lib/awx/venv") - -# Very important that this is editable (not read_only) in the API -AWX_ISOLATION_SHOW_PATHS = [ - '/etc/pki/ca-trust:/etc/pki/ca-trust:O', - '/usr/share/pki:/usr/share/pki:O', -] - -# Store a snapshot of default settings at this point before loading any -# customizable config files. -this_module = sys.modules[__name__] -local_vars = dir(this_module) -DEFAULTS_SNAPSHOT = {} # define after we save local_vars so we do not snapshot the snapshot -for setting in local_vars: - if setting.isupper(): - DEFAULTS_SNAPSHOT[setting] = copy.deepcopy(getattr(this_module, setting)) - -del local_vars # avoid temporary variables from showing up in dir(settings) -del this_module -# -############################################################################################### -# -# Any settings defined after this point will be marked as as a read_only database setting -# -################################################################################################ - -# 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_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') - -# Attempt to load settings from /etc/tower/settings.py first, followed by -# /etc/tower/conf.d/*.py. -try: - include(settings_file, optional(settings_files), scope=locals()) -except ImportError: - traceback.print_exc() - sys.exit(1) -except IOError: - from django.core.exceptions import ImproperlyConfigured - - included_file = locals().get('__included_file__', '') - if not included_file or included_file == settings_file: - # The import doesn't always give permission denied, so try to open the - # settings file directly. - try: - e = None - open(settings_file) - except IOError: - pass - if e and e.errno == errno.EACCES: - SECRET_KEY = 'permission-denied' - LOGGING = {} - else: - msg = 'No AWX configuration found at %s.' % settings_file - msg += '\nDefine the AWX_SETTINGS_FILE environment variable to ' - msg += 'specify an alternate path.' - raise ImproperlyConfigured(msg) - else: - raise - -# The below runs AFTER all of the custom settings are imported -# because conf.d files will define DATABASES and this should modify that -from .application_name import set_application_name -set_application_name(DATABASES, CLUSTER_HOST_ID) # NOQA +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "awx.settings") +os.environ.setdefault("AWX_MODE", "production") -del set_application_name +from ansible_base.lib.dynamic_config import export +from . import DYNACONF # noqa -# Set the value of any feature flags that are defined in the local settings -for feature in list(FLAGS.keys()): # noqa: F405 - if feature in locals(): - FLAGS[feature][0]['value'] = locals()[feature] # noqa: F405 +export(__name__, DYNACONF) diff --git a/awx/settings/production_defaults.py b/awx/settings/production_defaults.py new file mode 100644 index 000000000000..5599b81753ef --- /dev/null +++ b/awx/settings/production_defaults.py @@ -0,0 +1,30 @@ +# Copyright (c) 2015 Ansible, Inc. +# All Rights Reserved. + +# Production settings for AWX project. + +import os + +DEBUG = False +TEMPLATE_DEBUG = DEBUG +SQL_DEBUG = DEBUG + +# Clear database settings to force production environment to define them. +DATABASES = {} + +# Clear the secret key to force production environment to define it. +SECRET_KEY = None + +# Hosts/domain names that are valid for this site; required if DEBUG is False +# See https://docs.djangoproject.com/en/dev/ref/settings/#allowed-hosts +ALLOWED_HOSTS = [] + +# Ansible base virtualenv paths and enablement +# only used for deprecated fields and management commands for them +BASE_VENV_PATH = os.path.realpath("/var/lib/awx/venv") + +# Very important that this is editable (not read_only) in the API +AWX_ISOLATION_SHOW_PATHS = [ + '/etc/pki/ca-trust:/etc/pki/ca-trust:O', + '/usr/share/pki:/usr/share/pki:O', +] diff --git a/awx/settings/quiet_defaults.py b/awx/settings/quiet_defaults.py new file mode 100644 index 000000000000..1cb21720f7dd --- /dev/null +++ b/awx/settings/quiet_defaults.py @@ -0,0 +1,8 @@ +# Copyright (c) 2015 Ansible, Inc. +# All Rights Reserved. +# Development settings for AWX project, but with DEBUG disabled + +# Disable capturing DEBUG +DEBUG = False +TEMPLATE_DEBUG = DEBUG +SQL_DEBUG = DEBUG diff --git a/awx/urls.py b/awx/urls.py index daef360d5788..862dc5dcd9fd 100644 --- a/awx/urls.py +++ b/awx/urls.py @@ -37,7 +37,7 @@ def get_urlpatterns(prefix=None): re_path(r'^(?!api/).*', include('awx.ui.urls', namespace='ui')), ] - if settings.SETTINGS_MODULE == 'awx.settings.development': + if settings.DYNACONF.is_development_mode: try: import debug_toolbar diff --git a/licenses/dynaconf.txt b/licenses/dynaconf.txt new file mode 100644 index 000000000000..cbc6bbf2903b --- /dev/null +++ b/licenses/dynaconf.txt @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2015 Bruno Rocha + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/requirements/requirements.in b/requirements/requirements.in index 339a7d8e50fc..e8cc9b13cade 100644 --- a/requirements/requirements.in +++ b/requirements/requirements.in @@ -24,6 +24,7 @@ django-solo django-split-settings djangorestframework>=3.15.0 djangorestframework-yaml +dynaconf<4 filelock GitPython>=3.1.37 # CVE-2023-41040 grpcio diff --git a/requirements/requirements.txt b/requirements/requirements.txt index 18ea730743c1..b1700479d677 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -168,6 +168,8 @@ djangorestframework-yaml==2.0.0 # via -r /awx_devel/requirements/requirements.in durationpy==0.9 # via kubernetes +dynaconf==3.2.10 + # via -r /awx_devel/requirements/requirements.in enum-compat==0.0.3 # via asn1 filelock==3.16.1 diff --git a/requirements/requirements_git.txt b/requirements/requirements_git.txt index cfd354be4c70..2029c948c156 100644 --- a/requirements/requirements_git.txt +++ b/requirements/requirements_git.txt @@ -1,6 +1,7 @@ git+https://github.com/ansible/system-certifi.git@devel#egg=certifi # Remove pbr from requirements.in when moving ansible-runner to requirements.in git+https://github.com/ansible/ansible-runner.git@devel#egg=ansible-runner -django-ansible-base @ git+https://github.com/ansible/django-ansible-base@devel#egg=django-ansible-base[rest-filters,jwt_consumer,resource-registry,rbac,feature-flags] +# django-ansible-base @ git+https://github.com/ansible/django-ansible-base@devel#egg=django-ansible-base[rest-filters,jwt_consumer,resource-registry,rbac,feature-flags] +django-ansible-base @ git+https://github.com/rochacbruno/django-ansible-base@dynaconf_settings#egg=django-ansible-base[rest-filters,jwt_consumer,resource-registry,rbac,feature-flags] awx-plugins-core @ git+https://github.com/ansible/awx-plugins.git@devel#egg=awx-plugins-core awx_plugins.interfaces @ git+https://github.com/ansible/awx_plugins.interfaces.git diff --git a/tools/docker-compose/ansible/roles/sources/templates/docker-compose.yml.j2 b/tools/docker-compose/ansible/roles/sources/templates/docker-compose.yml.j2 index dd0b651f00b9..c224fdb940ee 100644 --- a/tools/docker-compose/ansible/roles/sources/templates/docker-compose.yml.j2 +++ b/tools/docker-compose/ansible/roles/sources/templates/docker-compose.yml.j2 @@ -42,6 +42,7 @@ services: DJANGO_SUPERUSER_PASSWORD: {{ admin_password }} UWSGI_MOUNT_PATH: {{ ingress_path }} DJANGO_COLORS: "${DJANGO_COLORS:-}" + DJANGO_SETTINGS_MODULE: "awx.settings" {% if loop.index == 1 %} RUN_MIGRATIONS: 1 {% endif %} diff --git a/tools/docker-compose/ansible/roles/sources/templates/local_settings.py.j2 b/tools/docker-compose/ansible/roles/sources/templates/local_settings.py.j2 index 1be38f43e28a..21a94378a28e 100644 --- a/tools/docker-compose/ansible/roles/sources/templates/local_settings.py.j2 +++ b/tools/docker-compose/ansible/roles/sources/templates/local_settings.py.j2 @@ -3,9 +3,14 @@ # Local Django settings for AWX project. -# All variables defined in awx/settings/development.py will already be loaded -# into the global namespace before this file is loaded, to allow for reading -# and updating the default settings as needed. +# This file is loaded by Dynaconf from awx/settings.py +# dynaconf reads only the UPPER case variables from this file +# To customize existing settings use dynaconf merging syntax +# e.g: +# DATABASES__default__NAME = 'custom_db_name' +# LOGGING__handlers__console__level = 'DEBUG' +# +# For more complex conditionals use Dynaconf Post Hook. ############################################################################### # MISC PROJECT SETTINGS @@ -35,30 +40,49 @@ OPTIONAL_API_URLPATTERN_PREFIX = '{{ api_urlpattern_prefix }}' # WARNING also logs 4xx responses. # Enable the following lines to turn on lots of permissions-related logging. -# LOGGING['loggers']['awx.main.access']['level'] = 'DEBUG' -# LOGGING['loggers']['awx.main.signals']['level'] = 'DEBUG' -# LOGGING['loggers']['awx.main.permissions']['level'] = 'DEBUG' - -# Enable the following line to turn on database settings logging. -# LOGGING['loggers']['awx.conf']['level'] = 'DEBUG' +# LOGGING__loggers = { +# "dynaconf_merge": True, +# 'awx.main.access': {'level': 'DEBUG'}, +# 'awx.main.signals': {'level': 'DEBUG'}, +# 'awx.main.permissions': {'level': 'DEBUG'}, +# # Enable the following line to turn on database settings logging. +# # "awx.conf": {'level': 'DEBUG'} +# } {% if enable_otel|bool %} -LOGGING['handlers']['otel'] |= { - 'class': 'awx.main.utils.handlers.OTLPHandler', - 'endpoint': 'http://otel:4317', -} -# Add otel log handler to all log handlers where propagate is False -for name in LOGGING['loggers'].keys(): - if not LOGGING['loggers'][name].get('propagate', True): - handler = LOGGING['loggers'][name].get('handlers', []) - if 'otel' not in handler: - LOGGING['loggers'][name].get('handlers', []).append('otel') - -# Everything without explicit propagate=False ends up logging to 'awx' so add it -handler = LOGGING['loggers']['awx'].get('handlers', []) -if 'otel' not in handler: - LOGGING['loggers']['awx'].get('handlers', []).append('otel') - +LOGGING__handlers__otel__class = 'awx.main.utils.handlers.OTLPHandler' +LOGGING__handlers__otel__endpoint = 'http://otel:4317' + +from dynaconf import post_hook + + +@post_hook +def set_log_handlers(settings) -> dict: + """Defers the setting of log handlers until after the settings are fully loaded. + this function is registered as a dynaconf hook, + this must return a dict that will be merged into the settings. + NOTE: `settings` is a read-only that contains the settings loaded so far. + """ + data = {} + if (logging := settings.get('LOGGING')) is not None: + loggers = logging.get('loggers', {}) + + # Add otel log handler to all log handlers where propagate is False + for logger in loggers.values(): + if logger.get('propagate', True): + continue + if (handlers := logger.get('handlers')) is not None: + if 'otel' not in handlers: + handlers.append('otel') + + + # Everything without explicit propagate=False ends up logging to 'awx' so add it + if (handlers := loggers.get('awx', {}).get('handlers')) is not None: + if 'otel' not in handlers: + handlers.append('otel') + + data['LOGGING'] = logging + return data {% endif %} BROADCAST_WEBSOCKET_PORT = 8013