From c566c332f990606c12ad2c185ce077b0863e7456 Mon Sep 17 00:00:00 2001 From: AlanCoding Date: Fri, 23 Aug 2019 16:46:48 -0400 Subject: [PATCH 01/12] Initial env var implementation of private galaxy server --- awx/main/conf.py | 55 +++++++++++++++++++ awx/main/tasks.py | 13 +++++ awx/settings/defaults.py | 9 +++ .../jobs-form/configuration-jobs.form.js | 7 +++ 4 files changed, 84 insertions(+) diff --git a/awx/main/conf.py b/awx/main/conf.py index 3c3f53cadc..3aa6046ea1 100644 --- a/awx/main/conf.py +++ b/awx/main/conf.py @@ -410,6 +410,15 @@ register( category_slug='system', ) +register( + 'PROJECT_UPDATE_VVV', + field_class=fields.BooleanField, + label=_('Run Project Updates With Higher Verbosity'), + help_text=_('Adds the CLI -vvv flag to ansible-playbook runs of project_update.yml used for project updates.'), + category=_('Jobs'), + category_slug='jobs', +) + register( 'AWX_ROLES_ENABLED', field_class=fields.BooleanField, @@ -430,6 +439,52 @@ register( category_slug='jobs', ) +register( + 'PRIVATE_GALAXY_URL', + field_class=fields.URLField, + label=_('Private Galaxy Server Host'), + help_text=_('For using a private galaxy server at higher precedence than the public Ansible Galaxy. ' + 'The URL of the galaxy instance to connect to, this is required if using a private galaxy server.'), + category=_('Jobs'), + category_slug='jobs', +) + +register( + 'PRIVATE_GALAXY_USERNAME', + field_class=fields.CharField, + label=_('Private Galaxy Server Username'), + help_text=_('For using a private galaxy server at higher precedence than the public Ansible Galaxy. ' + 'The username to use for basic authentication against the Galaxy instance, ' + 'this is mutually exclusive with PRIVATE_GALAXY_TOKEN.'), + category=_('Jobs'), + category_slug='jobs', + depends_on=['PRIVATE_GALAXY_URL'] +) + +register( + 'PRIVATE_GALAXY_PASSWORD', + field_class=fields.CharField, + label=_('Private Galaxy Server Password'), + help_text=_('For using a private galaxy server at higher precedence than the public Ansible Galaxy. ' + 'The password to use for basic authentication against the Galaxy instance, ' + 'this is mutually exclusive with PRIVATE_GALAXY_TOKEN.'), + category=_('Jobs'), + category_slug='jobs', + depends_on=['PRIVATE_GALAXY_URL'] +) + +register( + 'PRIVATE_GALAXY_TOKEN', + field_class=fields.CharField, + label=_('Private Galaxy Server Token'), + help_text=_('For using a private galaxy server at higher precedence than the public Ansible Galaxy. ' + 'The username to use for basic authentication against the Galaxy instance, ' + 'this is mutually exclusive with corresponding username and password settings.'), + category=_('Jobs'), + category_slug='jobs', + depends_on=['PRIVATE_GALAXY_URL'] +) + register( 'STDOUT_MAX_BYTES_DISPLAY', field_class=fields.IntegerField, diff --git a/awx/main/tasks.py b/awx/main/tasks.py index ad396a45cf..f7208deb54 100644 --- a/awx/main/tasks.py +++ b/awx/main/tasks.py @@ -1883,6 +1883,19 @@ class RunProjectUpdate(BaseTask): env['TMP'] = settings.AWX_PROOT_BASE_PATH env['PROJECT_UPDATE_ID'] = str(project_update.pk) env['ANSIBLE_CALLBACK_PLUGINS'] = self.get_path_to('..', 'plugins', 'callback') + private_galaxy_url = settings.PRIVATE_GALAXY_URL + if private_galaxy_url: + # set up the fallback server, which is the normal Ansible Galaxy + env['ANSIBLE_GALAXY_SERVER_GALAXY_URL'] = 'https://galaxy.ansible.com' + env['ANSIBLE_GALAXY_SERVER_PRIVATE_GALAXY_URL'] = private_galaxy_url + for key in ('url', 'username', 'password', 'token'): + setting_name = 'PRIVATE_GALAXY_{}'.format(key.upper()) + value = getattr(settings, setting_name) + if value: + env_key = 'ANSIBLE_GALAXY_SERVER_PRIVATE_GALAXY_{}'.format(key.upper()) + env[env_key] = value + # now set the precedence + env['ANSIBLE_GALAXY_SERVER_LIST'] = 'private_galaxy,galaxy' return env def _build_scm_url_extra_vars(self, project_update): diff --git a/awx/settings/defaults.py b/awx/settings/defaults.py index fb24b7014f..1424850cb2 100644 --- a/awx/settings/defaults.py +++ b/awx/settings/defaults.py @@ -609,6 +609,9 @@ AWX_REBUILD_SMART_MEMBERSHIP = False # By default, allow arbitrary Jinja templating in extra_vars defined on a Job Template ALLOW_JINJA_IN_EXTRA_VARS = 'template' +# Run project updates with extra verbosity +PROJECT_UPDATE_VVV = False + # Enable dynamically pulling roles from a requirement.yml file # when updating SCM projects # Note: This setting may be overridden by database settings. @@ -619,6 +622,12 @@ AWX_ROLES_ENABLED = True # Note: This setting may be overridden by database settings. AWX_COLLECTIONS_ENABLED = True +# Settings for private galaxy server, should be set in the UI +PRIVATE_GALAXY_URL = None +PRIVATE_GALAXY_USERNAME = None +PRIVATE_GALAXY_TOKEN = None +PRIVATE_GALAXY_PASSWORD = None + # Enable bubblewrap support for running jobs (playbook runs only). # Note: This setting may be overridden by database settings. AWX_PROOT_ENABLED = True diff --git a/awx/ui/client/src/configuration/forms/jobs-form/configuration-jobs.form.js b/awx/ui/client/src/configuration/forms/jobs-form/configuration-jobs.form.js index 4c844a7b7d..dcf4a7f97d 100644 --- a/awx/ui/client/src/configuration/forms/jobs-form/configuration-jobs.form.js +++ b/awx/ui/client/src/configuration/forms/jobs-form/configuration-jobs.form.js @@ -58,12 +58,19 @@ export default ['i18n', function(i18n) { type: 'text', reset: 'ANSIBLE_FACT_CACHE_TIMEOUT', }, + PROJECT_UPDATE_VVV: { + type: 'toggleSwitch', + }, AWX_ROLES_ENABLED: { type: 'toggleSwitch', }, AWX_COLLECTIONS_ENABLED: { type: 'toggleSwitch', }, + PRIVATE_GALAXY_URL: { + type: 'text', + reset: 'PRIVATE_GALAXY_URL', + }, AWX_TASK_ENV: { type: 'textarea', reset: 'AWX_TASK_ENV', From d59d8562db35a8b1d7769cc8081c84028779c931 Mon Sep 17 00:00:00 2001 From: AlanCoding Date: Mon, 26 Aug 2019 12:10:10 -0400 Subject: [PATCH 02/12] Avoid redacting Galaxy URLs --- awx/main/redact.py | 13 ++++++++++++- awx/settings/defaults.py | 2 ++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/awx/main/redact.py b/awx/main/redact.py index ec6211910b..fce725f427 100644 --- a/awx/main/redact.py +++ b/awx/main/redact.py @@ -1,6 +1,8 @@ import re import urllib.parse as urlparse +from django.conf import settings + REPLACE_STR = '$encrypted$' @@ -10,14 +12,22 @@ class UriCleaner(object): @staticmethod def remove_sensitive(cleartext): + if settings.PRIVATE_GALAXY_URL: + exclude_list = (settings.PUBLIC_GALAXY_URL, settings.PRIVATE_GALAXY_URL) + else: + exclude_list = (settings.PUBLIC_GALAXY_URL) redactedtext = cleartext text_index = 0 while True: match = UriCleaner.SENSITIVE_URI_PATTERN.search(redactedtext, text_index) if not match: break + uri_str = match.group(1) + # Do not redact items from the exclude list + if any(uri_str.startswith(exclude_uri) for exclude_uri in exclude_list): + text_index = match.start() + len(UriCleaner.REPLACE_STR) + continue try: - uri_str = match.group(1) # May raise a ValueError if invalid URI for one reason or another o = urlparse.urlsplit(uri_str) @@ -52,6 +62,7 @@ class UriCleaner(object): redactedtext = t if text_index >= len(redactedtext): text_index = len(redactedtext) - 1 + print('URL string old: {} new: {}'.format(uri_str_old, uri_str)) except ValueError: # Invalid URI, redact the whole URI to be safe redactedtext = redactedtext[:match.start()] + UriCleaner.REPLACE_STR + redactedtext[match.end():] diff --git a/awx/settings/defaults.py b/awx/settings/defaults.py index 1424850cb2..c6bbe841e9 100644 --- a/awx/settings/defaults.py +++ b/awx/settings/defaults.py @@ -627,6 +627,8 @@ PRIVATE_GALAXY_URL = None PRIVATE_GALAXY_USERNAME = None PRIVATE_GALAXY_TOKEN = None PRIVATE_GALAXY_PASSWORD = None +# Public Galaxy URL, not configurable outside of file-based settings +PUBLIC_GALAXY_URL = 'https://galaxy.ansible.com' # Enable bubblewrap support for running jobs (playbook runs only). # Note: This setting may be overridden by database settings. From 093bf6877be6b7b7ce34f8d8edfde83f2cbe3601 Mon Sep 17 00:00:00 2001 From: AlanCoding Date: Mon, 26 Aug 2019 12:40:43 -0400 Subject: [PATCH 03/12] Finish adding settings to UI --- awx/main/conf.py | 25 ++++++++++++------- awx/main/tasks.py | 2 +- awx/settings/defaults.py | 8 +++--- .../jobs-form/configuration-jobs.form.js | 14 +++++++++++ 4 files changed, 35 insertions(+), 14 deletions(-) diff --git a/awx/main/conf.py b/awx/main/conf.py index 3aa6046ea1..a4c144a351 100644 --- a/awx/main/conf.py +++ b/awx/main/conf.py @@ -442,47 +442,54 @@ register( register( 'PRIVATE_GALAXY_URL', field_class=fields.URLField, - label=_('Private Galaxy Server Host'), + required=False, + allow_blank=True, + label=_('Private Galaxy Server URL'), help_text=_('For using a private galaxy server at higher precedence than the public Ansible Galaxy. ' 'The URL of the galaxy instance to connect to, this is required if using a private galaxy server.'), category=_('Jobs'), - category_slug='jobs', + category_slug='jobs' ) register( 'PRIVATE_GALAXY_USERNAME', field_class=fields.CharField, + required=False, + allow_blank=True, label=_('Private Galaxy Server Username'), help_text=_('For using a private galaxy server at higher precedence than the public Ansible Galaxy. ' 'The username to use for basic authentication against the Galaxy instance, ' 'this is mutually exclusive with PRIVATE_GALAXY_TOKEN.'), category=_('Jobs'), - category_slug='jobs', - depends_on=['PRIVATE_GALAXY_URL'] + category_slug='jobs' ) register( 'PRIVATE_GALAXY_PASSWORD', field_class=fields.CharField, + encrypted=True, + required=False, + allow_blank=True, label=_('Private Galaxy Server Password'), help_text=_('For using a private galaxy server at higher precedence than the public Ansible Galaxy. ' 'The password to use for basic authentication against the Galaxy instance, ' 'this is mutually exclusive with PRIVATE_GALAXY_TOKEN.'), category=_('Jobs'), - category_slug='jobs', - depends_on=['PRIVATE_GALAXY_URL'] + category_slug='jobs' ) register( 'PRIVATE_GALAXY_TOKEN', field_class=fields.CharField, + encrypted=True, + required=False, + allow_blank=True, label=_('Private Galaxy Server Token'), help_text=_('For using a private galaxy server at higher precedence than the public Ansible Galaxy. ' - 'The username to use for basic authentication against the Galaxy instance, ' + 'The token to use for connecting with the Galaxy instance, ' 'this is mutually exclusive with corresponding username and password settings.'), category=_('Jobs'), - category_slug='jobs', - depends_on=['PRIVATE_GALAXY_URL'] + category_slug='jobs' ) register( diff --git a/awx/main/tasks.py b/awx/main/tasks.py index f7208deb54..f35154be59 100644 --- a/awx/main/tasks.py +++ b/awx/main/tasks.py @@ -1883,11 +1883,11 @@ class RunProjectUpdate(BaseTask): env['TMP'] = settings.AWX_PROOT_BASE_PATH env['PROJECT_UPDATE_ID'] = str(project_update.pk) env['ANSIBLE_CALLBACK_PLUGINS'] = self.get_path_to('..', 'plugins', 'callback') + # If private galaxy URL is non-blank, that means this feature is enabled private_galaxy_url = settings.PRIVATE_GALAXY_URL if private_galaxy_url: # set up the fallback server, which is the normal Ansible Galaxy env['ANSIBLE_GALAXY_SERVER_GALAXY_URL'] = 'https://galaxy.ansible.com' - env['ANSIBLE_GALAXY_SERVER_PRIVATE_GALAXY_URL'] = private_galaxy_url for key in ('url', 'username', 'password', 'token'): setting_name = 'PRIVATE_GALAXY_{}'.format(key.upper()) value = getattr(settings, setting_name) diff --git a/awx/settings/defaults.py b/awx/settings/defaults.py index c6bbe841e9..7adce8b8ae 100644 --- a/awx/settings/defaults.py +++ b/awx/settings/defaults.py @@ -623,10 +623,10 @@ AWX_ROLES_ENABLED = True AWX_COLLECTIONS_ENABLED = True # Settings for private galaxy server, should be set in the UI -PRIVATE_GALAXY_URL = None -PRIVATE_GALAXY_USERNAME = None -PRIVATE_GALAXY_TOKEN = None -PRIVATE_GALAXY_PASSWORD = None +PRIVATE_GALAXY_URL = '' +PRIVATE_GALAXY_USERNAME = '' +PRIVATE_GALAXY_TOKEN = '' +PRIVATE_GALAXY_PASSWORD = '' # Public Galaxy URL, not configurable outside of file-based settings PUBLIC_GALAXY_URL = 'https://galaxy.ansible.com' diff --git a/awx/ui/client/src/configuration/forms/jobs-form/configuration-jobs.form.js b/awx/ui/client/src/configuration/forms/jobs-form/configuration-jobs.form.js index dcf4a7f97d..4736c10de9 100644 --- a/awx/ui/client/src/configuration/forms/jobs-form/configuration-jobs.form.js +++ b/awx/ui/client/src/configuration/forms/jobs-form/configuration-jobs.form.js @@ -71,6 +71,20 @@ export default ['i18n', function(i18n) { type: 'text', reset: 'PRIVATE_GALAXY_URL', }, + PRIVATE_GALAXY_USERNAME: { + type: 'text', + reset: 'PRIVATE_GALAXY_USERNAME', + }, + PRIVATE_GALAXY_PASSWORD: { + type: 'sensitive', + hasShowInputButton: true, + reset: 'PRIVATE_GALAXY_PASSWORD', + }, + PRIVATE_GALAXY_TOKEN: { + type: 'sensitive', + hasShowInputButton: true, + reset: 'PRIVATE_GALAXY_TOKEN', + }, AWX_TASK_ENV: { type: 'textarea', reset: 'AWX_TASK_ENV', From 8bda048e6d0b2ffc35b1ce5b86d4c5feaba8aa5d Mon Sep 17 00:00:00 2001 From: AlanCoding Date: Tue, 27 Aug 2019 11:32:35 -0400 Subject: [PATCH 04/12] validate galaxy server settings involves some changes to the redact code --- awx/main/conf.py | 47 ++++++++++++++++++++++++++++++++++++++++++++++ awx/main/redact.py | 5 ++--- 2 files changed, 49 insertions(+), 3 deletions(-) diff --git a/awx/main/conf.py b/awx/main/conf.py index a4c144a351..efb6af7d21 100644 --- a/awx/main/conf.py +++ b/awx/main/conf.py @@ -762,4 +762,51 @@ def logging_validate(serializer, attrs): return attrs +def galaxy_validate(serializer, attrs): + """Ansible Galaxy config options have mutual exclusivity rules, these rules + are enforced here on serializer validation so that users will not be able + to save settings which obviously break all project updates. + """ + galaxy_fields = ('url', 'username', 'password', 'token') + if not any('PRIVATE_GALAXY_{}'.format(subfield.upper()) in attrs for subfield in galaxy_fields): + return attrs + + def _new_value(field_name): + if field_name in attrs: + return attrs[field_name] + elif not serializer.instance: + return '' + return getattr(serializer.instance, field_name, '') + + galaxy_data = {} + for subfield in galaxy_fields: + galaxy_data[subfield] = _new_value('PRIVATE_GALAXY_{}'.format(subfield.upper())) + errors = {} + print('galaxy data') + print(galaxy_data) + if not galaxy_data['url']: + for k, v in galaxy_data.items(): + if v: + setting_name = 'PRIVATE_GALAXY_{}'.format(k.upper()) + errors.setdefault(setting_name, []) + errors[setting_name].append(_( + 'Cannot provide field if PRIVATE_GALAXY_URL is not set.' + )) + + if (galaxy_data['password'] or galaxy_data['username']) and galaxy_data['token']: + for k in ('password', 'username', 'token'): + setting_name = 'PRIVATE_GALAXY_{}'.format(k.upper()) + if setting_name in attrs: + errors.setdefault(setting_name, []) + errors[setting_name].append(_( + 'Setting PRIVATE_GALAXY_TOKEN is mutually exclusive with ' + 'PRIVATE_GALAXY_USERNAME and PRIVATE_GALAXY_PASSWORD.' + )) + + if errors: + raise serializers.ValidationError(errors) + return attrs + + register_validate('logging', logging_validate) +register_validate('jobs', galaxy_validate) diff --git a/awx/main/redact.py b/awx/main/redact.py index fce725f427..dc9f060666 100644 --- a/awx/main/redact.py +++ b/awx/main/redact.py @@ -15,7 +15,7 @@ class UriCleaner(object): if settings.PRIVATE_GALAXY_URL: exclude_list = (settings.PUBLIC_GALAXY_URL, settings.PRIVATE_GALAXY_URL) else: - exclude_list = (settings.PUBLIC_GALAXY_URL) + exclude_list = (settings.PUBLIC_GALAXY_URL,) redactedtext = cleartext text_index = 0 while True: @@ -25,7 +25,7 @@ class UriCleaner(object): uri_str = match.group(1) # Do not redact items from the exclude list if any(uri_str.startswith(exclude_uri) for exclude_uri in exclude_list): - text_index = match.start() + len(UriCleaner.REPLACE_STR) + text_index = match.start() + len(uri_str) continue try: # May raise a ValueError if invalid URI for one reason or another @@ -62,7 +62,6 @@ class UriCleaner(object): redactedtext = t if text_index >= len(redactedtext): text_index = len(redactedtext) - 1 - print('URL string old: {} new: {}'.format(uri_str_old, uri_str)) except ValueError: # Invalid URI, redact the whole URI to be safe redactedtext = redactedtext[:match.start()] + UriCleaner.REPLACE_STR + redactedtext[match.end():] From 922e779a86b070b5394f413bf1bb97216db3b80d Mon Sep 17 00:00:00 2001 From: AlanCoding Date: Wed, 28 Aug 2019 10:21:50 -0400 Subject: [PATCH 05/12] Rename private to primary in galaxy settings use a setting for the public galaxy URL --- awx/main/conf.py | 52 ++++++++++--------- awx/main/redact.py | 4 +- awx/main/tasks.py | 11 ++-- awx/settings/defaults.py | 8 +-- .../jobs-form/configuration-jobs.form.js | 16 +++--- 5 files changed, 47 insertions(+), 44 deletions(-) diff --git a/awx/main/conf.py b/awx/main/conf.py index efb6af7d21..b95318685b 100644 --- a/awx/main/conf.py +++ b/awx/main/conf.py @@ -440,52 +440,56 @@ register( ) register( - 'PRIVATE_GALAXY_URL', + 'PRIMARY_GALAXY_URL', field_class=fields.URLField, required=False, allow_blank=True, - label=_('Private Galaxy Server URL'), - help_text=_('For using a private galaxy server at higher precedence than the public Ansible Galaxy. ' - 'The URL of the galaxy instance to connect to, this is required if using a private galaxy server.'), + label=_('Primary Galaxy Server URL'), + help_text=_( + 'For organizations that run their own Galaxy service, this gives the option to specify a ' + 'host as the primary galaxy server. Requirements will be downloaded from the primary if the ' + 'specific role or collection is available there. If the content is not avilable in the primary, ' + 'or if this field is left blank, it will default to galaxy.ansible.com.' + ), category=_('Jobs'), category_slug='jobs' ) register( - 'PRIVATE_GALAXY_USERNAME', + 'PRIMARY_GALAXY_USERNAME', field_class=fields.CharField, required=False, allow_blank=True, - label=_('Private Galaxy Server Username'), - help_text=_('For using a private galaxy server at higher precedence than the public Ansible Galaxy. ' + label=_('Primary Galaxy Server Username'), + help_text=_('For using a galaxy server at higher precedence than the public Ansible Galaxy. ' 'The username to use for basic authentication against the Galaxy instance, ' - 'this is mutually exclusive with PRIVATE_GALAXY_TOKEN.'), + 'this is mutually exclusive with PRIMARY_GALAXY_TOKEN.'), category=_('Jobs'), category_slug='jobs' ) register( - 'PRIVATE_GALAXY_PASSWORD', + 'PRIMARY_GALAXY_PASSWORD', field_class=fields.CharField, encrypted=True, required=False, allow_blank=True, - label=_('Private Galaxy Server Password'), - help_text=_('For using a private galaxy server at higher precedence than the public Ansible Galaxy. ' + label=_('Primary Galaxy Server Password'), + help_text=_('For using a galaxy server at higher precedence than the public Ansible Galaxy. ' 'The password to use for basic authentication against the Galaxy instance, ' - 'this is mutually exclusive with PRIVATE_GALAXY_TOKEN.'), + 'this is mutually exclusive with PRIMARY_GALAXY_TOKEN.'), category=_('Jobs'), category_slug='jobs' ) register( - 'PRIVATE_GALAXY_TOKEN', + 'PRIMARY_GALAXY_TOKEN', field_class=fields.CharField, encrypted=True, required=False, allow_blank=True, - label=_('Private Galaxy Server Token'), - help_text=_('For using a private galaxy server at higher precedence than the public Ansible Galaxy. ' + label=_('Primary Galaxy Server Token'), + help_text=_('For using a galaxy server at higher precedence than the public Ansible Galaxy. ' 'The token to use for connecting with the Galaxy instance, ' 'this is mutually exclusive with corresponding username and password settings.'), category=_('Jobs'), @@ -767,8 +771,10 @@ def galaxy_validate(serializer, attrs): are enforced here on serializer validation so that users will not be able to save settings which obviously break all project updates. """ + prefix = 'PRIMARY_GALAXY_' + galaxy_fields = ('url', 'username', 'password', 'token') - if not any('PRIVATE_GALAXY_{}'.format(subfield.upper()) in attrs for subfield in galaxy_fields): + if not any('{}{}'.format(prefix, subfield.upper()) in attrs for subfield in galaxy_fields): return attrs def _new_value(field_name): @@ -780,27 +786,25 @@ def galaxy_validate(serializer, attrs): galaxy_data = {} for subfield in galaxy_fields: - galaxy_data[subfield] = _new_value('PRIVATE_GALAXY_{}'.format(subfield.upper())) + galaxy_data[subfield] = _new_value('{}{}'.format(prefix, subfield.upper())) errors = {} - print('galaxy data') - print(galaxy_data) if not galaxy_data['url']: for k, v in galaxy_data.items(): if v: - setting_name = 'PRIVATE_GALAXY_{}'.format(k.upper()) + setting_name = '{}{}'.format(prefix, k.upper()) errors.setdefault(setting_name, []) errors[setting_name].append(_( - 'Cannot provide field if PRIVATE_GALAXY_URL is not set.' + 'Cannot provide field if PRIMARY_GALAXY_URL is not set.' )) if (galaxy_data['password'] or galaxy_data['username']) and galaxy_data['token']: for k in ('password', 'username', 'token'): - setting_name = 'PRIVATE_GALAXY_{}'.format(k.upper()) + setting_name = '{}{}'.format(prefix, k.upper()) if setting_name in attrs: errors.setdefault(setting_name, []) errors[setting_name].append(_( - 'Setting PRIVATE_GALAXY_TOKEN is mutually exclusive with ' - 'PRIVATE_GALAXY_USERNAME and PRIVATE_GALAXY_PASSWORD.' + 'Setting PRIMARY_GALAXY_TOKEN is mutually exclusive with ' + 'PRIMARY_GALAXY_USERNAME and PRIMARY_GALAXY_PASSWORD.' )) if errors: diff --git a/awx/main/redact.py b/awx/main/redact.py index dc9f060666..1cdd997dbd 100644 --- a/awx/main/redact.py +++ b/awx/main/redact.py @@ -12,8 +12,8 @@ class UriCleaner(object): @staticmethod def remove_sensitive(cleartext): - if settings.PRIVATE_GALAXY_URL: - exclude_list = (settings.PUBLIC_GALAXY_URL, settings.PRIVATE_GALAXY_URL) + if settings.PRIMARY_GALAXY_URL: + exclude_list = (settings.PUBLIC_GALAXY_URL, settings.PRIMARY_GALAXY_URL) else: exclude_list = (settings.PUBLIC_GALAXY_URL,) redactedtext = cleartext diff --git a/awx/main/tasks.py b/awx/main/tasks.py index f35154be59..161f9181f9 100644 --- a/awx/main/tasks.py +++ b/awx/main/tasks.py @@ -1884,18 +1884,17 @@ class RunProjectUpdate(BaseTask): env['PROJECT_UPDATE_ID'] = str(project_update.pk) env['ANSIBLE_CALLBACK_PLUGINS'] = self.get_path_to('..', 'plugins', 'callback') # If private galaxy URL is non-blank, that means this feature is enabled - private_galaxy_url = settings.PRIVATE_GALAXY_URL - if private_galaxy_url: + if settings.PRIMARY_GALAXY_URL: # set up the fallback server, which is the normal Ansible Galaxy - env['ANSIBLE_GALAXY_SERVER_GALAXY_URL'] = 'https://galaxy.ansible.com' + env['ANSIBLE_GALAXY_SERVER_GALAXY_URL'] = settings.PUBLIC_GALAXY_URL for key in ('url', 'username', 'password', 'token'): - setting_name = 'PRIVATE_GALAXY_{}'.format(key.upper()) + setting_name = 'PRIMARY_GALAXY_{}'.format(key.upper()) value = getattr(settings, setting_name) if value: - env_key = 'ANSIBLE_GALAXY_SERVER_PRIVATE_GALAXY_{}'.format(key.upper()) + env_key = 'ANSIBLE_GALAXY_SERVER_PRIMARY_GALAXY_{}'.format(key.upper()) env[env_key] = value # now set the precedence - env['ANSIBLE_GALAXY_SERVER_LIST'] = 'private_galaxy,galaxy' + env['ANSIBLE_GALAXY_SERVER_LIST'] = 'primary_galaxy,galaxy' return env def _build_scm_url_extra_vars(self, project_update): diff --git a/awx/settings/defaults.py b/awx/settings/defaults.py index 7adce8b8ae..1a1dbed73e 100644 --- a/awx/settings/defaults.py +++ b/awx/settings/defaults.py @@ -623,10 +623,10 @@ AWX_ROLES_ENABLED = True AWX_COLLECTIONS_ENABLED = True # Settings for private galaxy server, should be set in the UI -PRIVATE_GALAXY_URL = '' -PRIVATE_GALAXY_USERNAME = '' -PRIVATE_GALAXY_TOKEN = '' -PRIVATE_GALAXY_PASSWORD = '' +PRIMARY_GALAXY_URL = '' +PRIMARY_GALAXY_USERNAME = '' +PRIMARY_GALAXY_TOKEN = '' +PRIMARY_GALAXY_PASSWORD = '' # Public Galaxy URL, not configurable outside of file-based settings PUBLIC_GALAXY_URL = 'https://galaxy.ansible.com' diff --git a/awx/ui/client/src/configuration/forms/jobs-form/configuration-jobs.form.js b/awx/ui/client/src/configuration/forms/jobs-form/configuration-jobs.form.js index 4736c10de9..027afe9f4d 100644 --- a/awx/ui/client/src/configuration/forms/jobs-form/configuration-jobs.form.js +++ b/awx/ui/client/src/configuration/forms/jobs-form/configuration-jobs.form.js @@ -67,23 +67,23 @@ export default ['i18n', function(i18n) { AWX_COLLECTIONS_ENABLED: { type: 'toggleSwitch', }, - PRIVATE_GALAXY_URL: { + PRIMARY_GALAXY_URL: { type: 'text', - reset: 'PRIVATE_GALAXY_URL', + reset: 'PRIMARY_GALAXY_URL', }, - PRIVATE_GALAXY_USERNAME: { + PRIMARY_GALAXY_USERNAME: { type: 'text', - reset: 'PRIVATE_GALAXY_USERNAME', + reset: 'PRIMARY_GALAXY_USERNAME', }, - PRIVATE_GALAXY_PASSWORD: { + PRIMARY_GALAXY_PASSWORD: { type: 'sensitive', hasShowInputButton: true, - reset: 'PRIVATE_GALAXY_PASSWORD', + reset: 'PRIMARY_GALAXY_PASSWORD', }, - PRIVATE_GALAXY_TOKEN: { + PRIMARY_GALAXY_TOKEN: { type: 'sensitive', hasShowInputButton: true, - reset: 'PRIVATE_GALAXY_TOKEN', + reset: 'PRIMARY_GALAXY_TOKEN', }, AWX_TASK_ENV: { type: 'textarea', From 576ff1007e08d7fae65ace6ea835aafecc287f52 Mon Sep 17 00:00:00 2001 From: AlanCoding Date: Thu, 29 Aug 2019 10:35:38 -0400 Subject: [PATCH 06/12] Describe usage of primary galaxy server in docs --- docs/collections.md | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/docs/collections.md b/docs/collections.md index 23d805b74c..93d99889ed 100644 --- a/docs/collections.md +++ b/docs/collections.md @@ -52,3 +52,31 @@ Example of `tmp` directory where job is running: └── tmp_6wod58k ``` + +### Galaxy Server Selection + +Ansible core default settings will download collections from the public +Galaxy server at `https://galaxy.ansible.com`. For details on +how Galaxy servers are configured in Ansible in general see: + +https://docs.ansible.com/ansible/devel/user_guide/collections_using.html +(if "devel" link goes stale in the future, it is for Ansible 2.9) + +You can set a different server to be the primary Galaxy server to download +roles and collections from in AWX project updates. +This is done via the setting `PRIMARY_GALAXY_URL` and similar +`PRIMARY_GALAXY_xxxx` settings for authentication. + +If the `PRIMARY_GALAXY_URL` setting is not blank, then the server list is defined +to be `primary_galaxy,galaxy`. The `primary_galaxy` server definition uses the URL +from those settings, as well as username, password, and/or token if applicable. +the `galaxy` server definition uses public Galaxy (`https://galaxy.ansible.com`) +with no authentication. + +This configuration causes requirements to be downloaded from the user-specified +primary galaxy server if they are available there. If a requirement is +not available from the primary galaxy server, then it will fallback to +downloading it from the public Galaxy server. + +Even when these settings are enabled, this can still be bypassed for a specific +requirement by using the `source:` option, as described in Ansible documentation. From 85c99cc38a599c16c6489dfa111287384e125f06 Mon Sep 17 00:00:00 2001 From: AlanCoding Date: Wed, 2 Oct 2019 11:46:47 -0400 Subject: [PATCH 07/12] Redact env vars for Galaxy token or password --- awx/main/models/credential/__init__.py | 2 +- awx/main/tests/unit/test_tasks.py | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/awx/main/models/credential/__init__.py b/awx/main/models/credential/__init__.py index 7c84226d7d..ce3295cc69 100644 --- a/awx/main/models/credential/__init__.py +++ b/awx/main/models/credential/__init__.py @@ -64,7 +64,7 @@ def build_safe_env(env): for k, v in safe_env.items(): if k == 'AWS_ACCESS_KEY_ID': continue - elif k.startswith('ANSIBLE_') and not k.startswith('ANSIBLE_NET'): + elif k.startswith('ANSIBLE_') and not k.startswith('ANSIBLE_NET') and not k.startswith('ANSIBLE_GALAXY_SERVER'): continue elif hidden_re.search(k): safe_env[k] = HIDDEN_PASSWORD diff --git a/awx/main/tests/unit/test_tasks.py b/awx/main/tests/unit/test_tasks.py index 4c49af8b5d..77c13fffd2 100644 --- a/awx/main/tests/unit/test_tasks.py +++ b/awx/main/tests/unit/test_tasks.py @@ -130,6 +130,8 @@ def test_send_notifications_list(mock_notifications_filter, mock_job_get, mocker ('VMWARE_PASSWORD', 'SECRET'), ('API_SECRET', 'SECRET'), ('CALLBACK_CONNECTION', 'amqp://tower:password@localhost:5672/tower'), + ('ANSIBLE_GALAXY_SERVER_PRIMARY_GALAXY_PASSWORD', 'SECRET'), + ('ANSIBLE_GALAXY_SERVER_PRIMARY_GALAXY_TOKEN', 'SECRET'), ]) def test_safe_env_filtering(key, value): assert build_safe_env({key: value})[key] == tasks.HIDDEN_PASSWORD From c09039e9638736b6d533ff0f761d7e39a867e2dd Mon Sep 17 00:00:00 2001 From: AlanCoding Date: Fri, 4 Oct 2019 13:24:39 -0400 Subject: [PATCH 08/12] Add setting for auth_url Also adjust public galaxy URL setting to allow using only the primary Galaxy server Include auth_url in token exclusivity validation --- awx/main/conf.py | 22 ++++++++++++---- awx/main/constants.py | 4 +++ awx/main/redact.py | 4 +-- awx/main/tasks.py | 26 ++++++++++++------- awx/settings/defaults.py | 15 ++++++++--- .../jobs-form/configuration-jobs.form.js | 4 +++ 6 files changed, 55 insertions(+), 20 deletions(-) diff --git a/awx/main/conf.py b/awx/main/conf.py index b95318685b..cacf577b24 100644 --- a/awx/main/conf.py +++ b/awx/main/conf.py @@ -496,6 +496,18 @@ register( category_slug='jobs' ) +register( + 'PRIMARY_GALAXY_AUTH_URL', + field_class=fields.CharField, + required=False, + allow_blank=True, + label=_('Primary Galaxy Authentication URL'), + help_text=_('For using a galaxy server at higher precedence than the public Ansible Galaxy. ' + 'The token_endpoint of a Keycloak server.'), + category=_('Jobs'), + category_slug='jobs' +) + register( 'STDOUT_MAX_BYTES_DISPLAY', field_class=fields.IntegerField, @@ -773,8 +785,8 @@ def galaxy_validate(serializer, attrs): """ prefix = 'PRIMARY_GALAXY_' - galaxy_fields = ('url', 'username', 'password', 'token') - if not any('{}{}'.format(prefix, subfield.upper()) in attrs for subfield in galaxy_fields): + from awx.main.constants import GALAXY_SERVER_FIELDS + if not any('{}{}'.format(prefix, subfield.upper()) in attrs for subfield in GALAXY_SERVER_FIELDS): return attrs def _new_value(field_name): @@ -785,7 +797,7 @@ def galaxy_validate(serializer, attrs): return getattr(serializer.instance, field_name, '') galaxy_data = {} - for subfield in galaxy_fields: + for subfield in GALAXY_SERVER_FIELDS: galaxy_data[subfield] = _new_value('{}{}'.format(prefix, subfield.upper())) errors = {} if not galaxy_data['url']: @@ -797,8 +809,8 @@ def galaxy_validate(serializer, attrs): 'Cannot provide field if PRIMARY_GALAXY_URL is not set.' )) - if (galaxy_data['password'] or galaxy_data['username']) and galaxy_data['token']: - for k in ('password', 'username', 'token'): + if (galaxy_data['password'] or galaxy_data['username']) and (galaxy_data['token'] or galaxy_data['auth_url']): + for k in ('password', 'username', 'token', 'auth_url'): setting_name = '{}{}'.format(prefix, k.upper()) if setting_name in attrs: errors.setdefault(setting_name, []) diff --git a/awx/main/constants.py b/awx/main/constants.py index eea1a55820..4c98d264dd 100644 --- a/awx/main/constants.py +++ b/awx/main/constants.py @@ -51,3 +51,7 @@ LOGGER_BLACKLIST = ( # loggers that may be called getting logging settings 'awx.conf' ) + +# these correspond to both AWX and Ansible settings to keep naming consistent +# for instance, settings.PRIMARY_GALAXY_AUTH_URL vs env var ANSIBLE_GALAXY_SERVER_FOO_AUTH_URL +GALAXY_SERVER_FIELDS = ('url', 'username', 'password', 'token', 'auth_url') diff --git a/awx/main/redact.py b/awx/main/redact.py index 1cdd997dbd..ae60684377 100644 --- a/awx/main/redact.py +++ b/awx/main/redact.py @@ -13,9 +13,9 @@ class UriCleaner(object): @staticmethod def remove_sensitive(cleartext): if settings.PRIMARY_GALAXY_URL: - exclude_list = (settings.PUBLIC_GALAXY_URL, settings.PRIMARY_GALAXY_URL) + exclude_list = [settings.PRIMARY_GALAXY_URL] + [server['url'] for server in settings.FALLBACK_GALAXY_SERVERS] else: - exclude_list = (settings.PUBLIC_GALAXY_URL,) + exclude_list = [server['url'] for server in settings.FALLBACK_GALAXY_SERVERS] redactedtext = cleartext text_index = 0 while True: diff --git a/awx/main/tasks.py b/awx/main/tasks.py index 161f9181f9..eb2d48546d 100644 --- a/awx/main/tasks.py +++ b/awx/main/tasks.py @@ -52,7 +52,7 @@ import ansible_runner # AWX from awx import __version__ as awx_application_version -from awx.main.constants import CLOUD_PROVIDERS, PRIVILEGE_ESCALATION_METHODS, STANDARD_INVENTORY_UPDATE_ENV +from awx.main.constants import CLOUD_PROVIDERS, PRIVILEGE_ESCALATION_METHODS, STANDARD_INVENTORY_UPDATE_ENV, GALAXY_SERVER_FIELDS from awx.main.access import access_registry from awx.main.models import ( Schedule, TowerScheduleState, Instance, InstanceGroup, @@ -1883,18 +1883,24 @@ class RunProjectUpdate(BaseTask): env['TMP'] = settings.AWX_PROOT_BASE_PATH env['PROJECT_UPDATE_ID'] = str(project_update.pk) env['ANSIBLE_CALLBACK_PLUGINS'] = self.get_path_to('..', 'plugins', 'callback') + env['ANSIBLE_GALAXY_IGNORE'] = True + # Set up the fallback server, which is the normal Ansible Galaxy by default + galaxy_servers = list(settings.FALLBACK_GALAXY_SERVERS) # If private galaxy URL is non-blank, that means this feature is enabled if settings.PRIMARY_GALAXY_URL: - # set up the fallback server, which is the normal Ansible Galaxy - env['ANSIBLE_GALAXY_SERVER_GALAXY_URL'] = settings.PUBLIC_GALAXY_URL - for key in ('url', 'username', 'password', 'token'): - setting_name = 'PRIMARY_GALAXY_{}'.format(key.upper()) - value = getattr(settings, setting_name) + galaxy_servers = [{'id': 'primary_galaxy'}] + galaxy_servers + for key in GALAXY_SERVER_FIELDS: + value = getattr(settings, 'PRIMARY_GALAXY_{}'.format(key.upper())) if value: - env_key = 'ANSIBLE_GALAXY_SERVER_PRIMARY_GALAXY_{}'.format(key.upper()) - env[env_key] = value - # now set the precedence - env['ANSIBLE_GALAXY_SERVER_LIST'] = 'primary_galaxy,galaxy' + galaxy_servers[0][key] = value + for server in galaxy_servers: + for key in GALAXY_SERVER_FIELDS: + if not server.get(key): + continue + env_key = ('ANSIBLE_GALAXY_SERVER_{}_{}'.format(server.get('id', 'unnamed'), key)).upper() + env[env_key] = server[key] + # now set the precedence of galaxy servers + env['ANSIBLE_GALAXY_SERVER_LIST'] = ','.join([server.get('id', 'unnamed') for server in galaxy_servers]) return env def _build_scm_url_extra_vars(self, project_update): diff --git a/awx/settings/defaults.py b/awx/settings/defaults.py index 1a1dbed73e..8d8ba9f008 100644 --- a/awx/settings/defaults.py +++ b/awx/settings/defaults.py @@ -622,13 +622,22 @@ AWX_ROLES_ENABLED = True # Note: This setting may be overridden by database settings. AWX_COLLECTIONS_ENABLED = True -# Settings for private galaxy server, should be set in the UI +# Settings for primary galaxy server, should be set in the UI PRIMARY_GALAXY_URL = '' PRIMARY_GALAXY_USERNAME = '' PRIMARY_GALAXY_TOKEN = '' PRIMARY_GALAXY_PASSWORD = '' -# Public Galaxy URL, not configurable outside of file-based settings -PUBLIC_GALAXY_URL = 'https://galaxy.ansible.com' +PRIMARY_GALAXY_AUTH_URL = '' +# Settings for the fallback galaxy server(s), normally this is the +# actual Ansible Galaxy site. +# server options: 'id', 'url', 'username', 'password', 'token', 'auth_url' +# To not use any fallback servers set this to [] +FALLBACK_GALAXY_SERVERS = [ + { + 'id': 'galaxy', + 'url': 'https://galaxy.ansible.com' + } +] # Enable bubblewrap support for running jobs (playbook runs only). # Note: This setting may be overridden by database settings. diff --git a/awx/ui/client/src/configuration/forms/jobs-form/configuration-jobs.form.js b/awx/ui/client/src/configuration/forms/jobs-form/configuration-jobs.form.js index 027afe9f4d..445b0864a2 100644 --- a/awx/ui/client/src/configuration/forms/jobs-form/configuration-jobs.form.js +++ b/awx/ui/client/src/configuration/forms/jobs-form/configuration-jobs.form.js @@ -85,6 +85,10 @@ export default ['i18n', function(i18n) { hasShowInputButton: true, reset: 'PRIMARY_GALAXY_TOKEN', }, + PRIMARY_GALAXY_AUTH_URL: { + type: 'text', + reset: 'PRIMARY_GALAXY_AUTH_URL', + }, AWX_TASK_ENV: { type: 'textarea', reset: 'AWX_TASK_ENV', From 06c62c48611d2658a89cd65414e0871c718c02b3 Mon Sep 17 00:00:00 2001 From: AlanCoding Date: Mon, 7 Oct 2019 14:52:04 -0400 Subject: [PATCH 09/12] update docs for galaxy auth URL material --- docs/collections.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/collections.md b/docs/collections.md index 93d99889ed..5cbf3eab7a 100644 --- a/docs/collections.md +++ b/docs/collections.md @@ -5,14 +5,14 @@ AWX supports the use of Ansible Collections. This section will give ways to use ### Project Collections Requirements If you specify a Collections requirements file in SCM at `collections/requirements.yml`, -then AWX will install Collections in that file in the implicit project sync -before a job run. The invocation is: +then AWX will install Collections from that file in the implicit project sync +before a job run. The invocation looks like: ``` -ansible-galaxy collection install -r requirements.yml -p +ansible-galaxy collection install -r requirements.yml -p /requirements_collections ``` -Example of `tmp` directory where job is running: +Example of the resultant `tmp` directory where job is running: ``` ├── project @@ -69,7 +69,7 @@ This is done via the setting `PRIMARY_GALAXY_URL` and similar If the `PRIMARY_GALAXY_URL` setting is not blank, then the server list is defined to be `primary_galaxy,galaxy`. The `primary_galaxy` server definition uses the URL -from those settings, as well as username, password, and/or token if applicable. +from those settings, as well as username, password, and/or token and auth_url if applicable. the `galaxy` server definition uses public Galaxy (`https://galaxy.ansible.com`) with no authentication. From 0594bdf650d7965e8245fa1a187c3c2f00f7c7e9 Mon Sep 17 00:00:00 2001 From: AlanCoding Date: Mon, 7 Oct 2019 20:03:40 -0400 Subject: [PATCH 10/12] Add more galaxy server param validation --- awx/main/conf.py | 39 ++++++++++++++++++++++++++++++++------- 1 file changed, 32 insertions(+), 7 deletions(-) diff --git a/awx/main/conf.py b/awx/main/conf.py index cacf577b24..b0284a70ea 100644 --- a/awx/main/conf.py +++ b/awx/main/conf.py @@ -2,6 +2,7 @@ import json import logging import os +from distutils.version import LooseVersion as Version # Django from django.utils.translation import ugettext_lazy as _ @@ -789,12 +790,12 @@ def galaxy_validate(serializer, attrs): if not any('{}{}'.format(prefix, subfield.upper()) in attrs for subfield in GALAXY_SERVER_FIELDS): return attrs - def _new_value(field_name): - if field_name in attrs: - return attrs[field_name] + def _new_value(setting_name): + if setting_name in attrs: + return attrs[setting_name] elif not serializer.instance: return '' - return getattr(serializer.instance, field_name, '') + return getattr(serializer.instance, setting_name, '') galaxy_data = {} for subfield in GALAXY_SERVER_FIELDS: @@ -808,16 +809,40 @@ def galaxy_validate(serializer, attrs): errors[setting_name].append(_( 'Cannot provide field if PRIMARY_GALAXY_URL is not set.' )) - + for k in GALAXY_SERVER_FIELDS: + if galaxy_data[k]: + setting_name = '{}{}'.format(prefix, k.upper()) + if (not serializer.instance) or (not getattr(serializer.instance, setting_name, '')): + # new auth is applied, so check if compatible with version + from awx.main.utils import get_ansible_version + current_version = get_ansible_version() + min_version = '2.9' + if Version(current_version) < Version(min_version): + errors.setdefault(setting_name, []) + errors[setting_name].append(_( + 'Galaxy server settings are not available until Ansible {min_version}, ' + 'you are running {current_version}.' + ).format(min_version=min_version, current_version=current_version)) if (galaxy_data['password'] or galaxy_data['username']) and (galaxy_data['token'] or galaxy_data['auth_url']): for k in ('password', 'username', 'token', 'auth_url'): setting_name = '{}{}'.format(prefix, k.upper()) if setting_name in attrs: errors.setdefault(setting_name, []) errors[setting_name].append(_( - 'Setting PRIMARY_GALAXY_TOKEN is mutually exclusive with ' - 'PRIMARY_GALAXY_USERNAME and PRIMARY_GALAXY_PASSWORD.' + 'Setting Galaxy token and authentication URL is mutually exclusive with username and password.' )) + if bool(galaxy_data['username']) != bool(galaxy_data['password']): + msg = _('If authenticating via username and password, both must be provided.') + for k in ('username', 'password'): + setting_name = '{}{}'.format(prefix, k.upper()) + errors.setdefault(setting_name, []) + errors[setting_name].append(msg) + if bool(galaxy_data['token']) != bool(galaxy_data['auth_url']): + msg = _('If authenticating via token, both token and authentication URL must be provided.') + for k in ('token', 'auth_url'): + setting_name = '{}{}'.format(prefix, k.upper()) + errors.setdefault(setting_name, []) + errors[setting_name].append(msg) if errors: raise serializers.ValidationError(errors) From e0e9c8321b223b8cc4b6aceb7225a2bab9ba109c Mon Sep 17 00:00:00 2001 From: AlanCoding Date: Tue, 8 Oct 2019 11:00:57 -0400 Subject: [PATCH 11/12] bump required version to 2.10 --- awx/main/conf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/awx/main/conf.py b/awx/main/conf.py index b0284a70ea..a310950f4f 100644 --- a/awx/main/conf.py +++ b/awx/main/conf.py @@ -816,7 +816,7 @@ def galaxy_validate(serializer, attrs): # new auth is applied, so check if compatible with version from awx.main.utils import get_ansible_version current_version = get_ansible_version() - min_version = '2.9' + min_version = '2.10' if Version(current_version) < Version(min_version): errors.setdefault(setting_name, []) errors[setting_name].append(_( From f10296b1b740b40c9470f5ea39cfb3732b019363 Mon Sep 17 00:00:00 2001 From: AlanCoding Date: Tue, 8 Oct 2019 11:20:57 -0400 Subject: [PATCH 12/12] Revert "bump required version to 2.10" This reverts commit e0e9c8321b223b8cc4b6aceb7225a2bab9ba109c. --- awx/main/conf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/awx/main/conf.py b/awx/main/conf.py index a310950f4f..b0284a70ea 100644 --- a/awx/main/conf.py +++ b/awx/main/conf.py @@ -816,7 +816,7 @@ def galaxy_validate(serializer, attrs): # new auth is applied, so check if compatible with version from awx.main.utils import get_ansible_version current_version = get_ansible_version() - min_version = '2.10' + min_version = '2.9' if Version(current_version) < Version(min_version): errors.setdefault(setting_name, []) errors[setting_name].append(_(