diff options
author | John Westcott IV <32551173+john-westcott-iv@users.noreply.github.com> | 2022-10-03 16:47:51 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2022-10-03 16:47:51 +0200 |
commit | 534763727f58b5e06153f6d0db01553bfcf7823a (patch) | |
tree | 6a583a5d343f8b3f892db147a48c19ed37464db6 | |
parent | fix name to be consistent (#12975) (diff) | |
parent | Updating migration file again (diff) | |
download | awx-534763727f58b5e06153f6d0db01553bfcf7823a.tar.xz awx-534763727f58b5e06153f6d0db01553bfcf7823a.zip |
Merge pull request #12728 from john-westcott-iv/ig_fallback21.7.0
Adding prevent_instance_group_fallback
21 files changed, 456 insertions, 291 deletions
diff --git a/awx/api/serializers.py b/awx/api/serializers.py index eaad921eb2..d5bb2cfe6b 100644 --- a/awx/api/serializers.py +++ b/awx/api/serializers.py @@ -1680,6 +1680,7 @@ class InventorySerializer(LabelsListMixin, BaseSerializerWithVariables): 'total_inventory_sources', 'inventory_sources_with_failures', 'pending_deletion', + 'prevent_instance_group_fallback', ) def get_related(self, obj): @@ -2937,6 +2938,7 @@ class JobTemplateSerializer(JobTemplateMixin, UnifiedJobTemplateSerializer, JobO 'job_slice_count', 'webhook_service', 'webhook_credential', + 'prevent_instance_group_fallback', ) read_only_fields = ('*', 'custom_virtualenv') diff --git a/awx/main/migrations/0172_prevent_instance_fallback.py b/awx/main/migrations/0172_prevent_instance_fallback.py new file mode 100644 index 0000000000..aeb3ad3ebb --- /dev/null +++ b/awx/main/migrations/0172_prevent_instance_fallback.py @@ -0,0 +1,29 @@ +# Generated by Django 3.2.13 on 2022-09-29 18:10 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('main', '0171_add_health_check_started'), + ] + + operations = [ + migrations.AddField( + model_name='inventory', + name='prevent_instance_group_fallback', + field=models.BooleanField( + default=False, + help_text='If enabled, the inventory will prevent adding any organization instance groups to the list of preferred instances groups to run associated job templates on.If this setting is enabled and you provided an empty list, the global instance groups will be applied.', + ), + ), + migrations.AddField( + model_name='jobtemplate', + name='prevent_instance_group_fallback', + field=models.BooleanField( + default=False, + help_text='If enabled, the job template will prevent adding any inventory or organization instance groups to the list of preferred instances groups to run on.If this setting is enabled and you provided an empty list, the global instance groups will be applied.', + ), + ), + ] diff --git a/awx/main/models/ad_hoc_commands.py b/awx/main/models/ad_hoc_commands.py index 7543162080..3b71119031 100644 --- a/awx/main/models/ad_hoc_commands.py +++ b/awx/main/models/ad_hoc_commands.py @@ -228,15 +228,14 @@ class AdHocCommand(UnifiedJob, JobNotificationMixin): @property def preferred_instance_groups(self): - if self.inventory is not None and self.inventory.organization is not None: - organization_groups = [x for x in self.inventory.organization.instance_groups.all()] - else: - organization_groups = [] + selected_groups = [] if self.inventory is not None: - inventory_groups = [x for x in self.inventory.instance_groups.all()] - else: - inventory_groups = [] - selected_groups = inventory_groups + organization_groups + for instance_group in self.inventory.instance_groups.all(): + selected_groups.append(instance_group) + if not self.inventory.prevent_instance_group_fallback and self.inventory.organization is not None: + for instance_group in self.inventory.organization.instance_groups.all(): + selected_groups.append(instance_group) + if not selected_groups: return self.global_instance_groups return selected_groups diff --git a/awx/main/models/inventory.py b/awx/main/models/inventory.py index 4a90ad5bad..54c7d3e2b1 100644 --- a/awx/main/models/inventory.py +++ b/awx/main/models/inventory.py @@ -63,7 +63,7 @@ class Inventory(CommonModelNameNotUnique, ResourceMixin, RelatedJobsMixin): an inventory source contains lists and hosts. """ - FIELDS_TO_PRESERVE_AT_COPY = ['hosts', 'groups', 'instance_groups'] + FIELDS_TO_PRESERVE_AT_COPY = ['hosts', 'groups', 'instance_groups', 'prevent_instance_group_fallback'] KIND_CHOICES = [ ('', _('Hosts have a direct link to this inventory.')), ('smart', _('Hosts for inventory generated using the host_filter property.')), @@ -175,6 +175,16 @@ class Inventory(CommonModelNameNotUnique, ResourceMixin, RelatedJobsMixin): related_name='inventory_labels', help_text=_('Labels associated with this inventory.'), ) + prevent_instance_group_fallback = models.BooleanField( + default=False, + help_text=( + "If enabled, the inventory will prevent adding any organization " + "instance groups to the list of preferred instances groups to run " + "associated job templates on." + "If this setting is enabled and you provided an empty list, the global instance " + "groups will be applied." + ), + ) def get_absolute_url(self, request=None): return reverse('api:inventory_detail', kwargs={'pk': self.pk}, request=request) @@ -1268,15 +1278,19 @@ class InventoryUpdate(UnifiedJob, InventorySourceOptions, JobNotificationMixin, @property def preferred_instance_groups(self): - if self.inventory_source.inventory is not None and self.inventory_source.inventory.organization is not None: - organization_groups = [x for x in self.inventory_source.inventory.organization.instance_groups.all()] - else: - organization_groups = [] + selected_groups = [] if self.inventory_source.inventory is not None: - inventory_groups = [x for x in self.inventory_source.inventory.instance_groups.all()] - else: - inventory_groups = [] - selected_groups = inventory_groups + organization_groups + # Add the inventory sources IG to the selected IGs first + for instance_group in self.inventory_source.inventory.instance_groups.all(): + selected_groups.append(instance_group) + # If the inventory allows for fallback and we have an organization then also append the orgs IGs to the end of the list + if ( + not getattr(self.inventory_source.inventory, 'prevent_instance_group_fallback', False) + and self.inventory_source.inventory.organization is not None + ): + for instance_group in self.inventory_source.inventory.organization.instance_groups.all(): + selected_groups.append(instance_group) + if not selected_groups: return self.global_instance_groups return selected_groups diff --git a/awx/main/models/jobs.py b/awx/main/models/jobs.py index b954c76e35..a84a5a67eb 100644 --- a/awx/main/models/jobs.py +++ b/awx/main/models/jobs.py @@ -203,7 +203,7 @@ class JobTemplate(UnifiedJobTemplate, JobOptions, SurveyJobTemplateMixin, Resour playbook) to an inventory source with a given credential. """ - FIELDS_TO_PRESERVE_AT_COPY = ['labels', 'instance_groups', 'credentials', 'survey_spec'] + FIELDS_TO_PRESERVE_AT_COPY = ['labels', 'instance_groups', 'credentials', 'survey_spec', 'prevent_instance_group_fallback'] FIELDS_TO_DISCARD_AT_COPY = ['vault_credential', 'credential'] SOFT_UNIQUE_TOGETHER = [('polymorphic_ctype', 'name', 'organization')] @@ -274,6 +274,15 @@ class JobTemplate(UnifiedJobTemplate, JobOptions, SurveyJobTemplateMixin, Resour 'admin_role', ], ) + prevent_instance_group_fallback = models.BooleanField( + default=False, + help_text=( + "If enabled, the job template will prevent adding any inventory or organization " + "instance groups to the list of preferred instances groups to run on." + "If this setting is enabled and you provided an empty list, the global instance " + "groups will be applied." + ), + ) @classmethod def _get_unified_job_class(cls): @@ -797,19 +806,13 @@ class Job(UnifiedJob, JobOptions, SurveyJobMixin, JobNotificationMixin, TaskMana def preferred_instance_groups(self): # If the user specified instance groups those will be handled by the unified_job.create_unified_job # This function handles only the defaults for a template w/o user specification - if self.organization is not None: - organization_groups = [x for x in self.organization.instance_groups.all()] - else: - organization_groups = [] - if self.inventory is not None: - inventory_groups = [x for x in self.inventory.instance_groups.all()] - else: - inventory_groups = [] - if self.job_template is not None: - template_groups = [x for x in self.job_template.instance_groups.all()] - else: - template_groups = [] - selected_groups = template_groups + inventory_groups + organization_groups + selected_groups = [] + for obj_type in ['job_template', 'inventory', 'organization']: + if getattr(self, obj_type) is not None: + for instance_group in getattr(self, obj_type).instance_groups.all(): + selected_groups.append(instance_group) + if getattr(getattr(self, obj_type), 'prevent_instance_group_fallback', False): + break if not selected_groups: return self.global_instance_groups return selected_groups diff --git a/awx/main/tests/functional/test_copy.py b/awx/main/tests/functional/test_copy.py index 9be8d6574c..0574f9ccbd 100644 --- a/awx/main/tests/functional/test_copy.py +++ b/awx/main/tests/functional/test_copy.py @@ -19,6 +19,7 @@ def test_job_template_copy( job_template_with_survey_passwords.inventory = inventory job_template_with_survey_passwords.labels.add(label) job_template_with_survey_passwords.instance_groups.add(ig) + job_template_with_survey_passwords.prevent_instance_group_fallback = True job_template_with_survey_passwords.save() job_template_with_survey_passwords.credentials.add(credential) job_template_with_survey_passwords.credentials.add(machine_credential) @@ -65,6 +66,7 @@ def test_job_template_copy( assert jt_copy.labels.get(pk=label.pk) == label assert jt_copy.instance_groups.count() != 0 assert jt_copy.instance_groups.get(pk=ig.pk) == ig + assert jt_copy.prevent_instance_group_fallback == True @pytest.mark.django_db @@ -95,6 +97,8 @@ def test_inventory_copy(inventory, group_factory, post, get, alice, organization host = group_1_1.hosts.create(name='host', inventory=inventory) group_2_1.hosts.add(host) inventory.admin_role.members.add(alice) + inventory.prevent_instance_group_fallback = True + inventory.save() assert get(reverse('api:inventory_copy', kwargs={'pk': inventory.pk}), alice, expect=200).data['can_copy'] is False inventory.organization.admin_role.members.add(alice) assert get(reverse('api:inventory_copy', kwargs={'pk': inventory.pk}), alice, expect=200).data['can_copy'] is True @@ -110,6 +114,7 @@ def test_inventory_copy(inventory, group_factory, post, get, alice, organization assert inventory_copy.organization == organization assert inventory_copy.created_by == alice assert inventory_copy.name == 'new inv name' + assert inventory_copy.prevent_instance_group_fallback == True assert set(group_1_1_copy.parents.all()) == set() assert set(group_2_1_copy.parents.all()) == set([group_1_1_copy]) assert set(group_2_2_copy.parents.all()) == set([group_1_1_copy, group_2_1_copy]) diff --git a/awx/main/tests/functional/test_instances.py b/awx/main/tests/functional/test_instances.py index e704de8971..8ce6524d38 100644 --- a/awx/main/tests/functional/test_instances.py +++ b/awx/main/tests/functional/test_instances.py @@ -391,6 +391,8 @@ class TestInstanceGroupOrdering: assert ad_hoc.preferred_instance_groups == [ig_org] inventory.instance_groups.add(ig_inv) assert ad_hoc.preferred_instance_groups == [ig_inv, ig_org] + inventory.prevent_instance_group_fallback = True + assert ad_hoc.preferred_instance_groups == [ig_inv] def test_inventory_update_instance_groups(self, instance_group_factory, inventory_source, default_instance_group): iu = InventoryUpdate.objects.create(inventory_source=inventory_source, source=inventory_source.source) @@ -404,6 +406,8 @@ class TestInstanceGroupOrdering: inventory_source.instance_groups.add(ig_tmp) # API does not allow setting IGs on inventory source, so ignore those assert iu.preferred_instance_groups == [ig_inv, ig_org] + inventory_source.inventory.prevent_instance_group_fallback = True + assert iu.preferred_instance_groups == [ig_inv] def test_job_instance_groups(self, instance_group_factory, inventory, project, default_instance_group): jt = JobTemplate.objects.create(inventory=inventory, project=project) diff --git a/awx/ui/src/screens/Inventory/InventoryDetail/InventoryDetail.js b/awx/ui/src/screens/Inventory/InventoryDetail/InventoryDetail.js index b25630cb89..6944707a6d 100644 --- a/awx/ui/src/screens/Inventory/InventoryDetail/InventoryDetail.js +++ b/awx/ui/src/screens/Inventory/InventoryDetail/InventoryDetail.js @@ -2,7 +2,14 @@ import React, { useCallback, useEffect } from 'react'; import { Link, useHistory } from 'react-router-dom'; import { t } from '@lingui/macro'; -import { Button, Chip } from '@patternfly/react-core'; +import { + Button, + Chip, + TextList, + TextListItem, + TextListItemVariants, + TextListVariants, +} from '@patternfly/react-core'; import AlertModal from 'components/AlertModal'; import { CardBody, CardActionsRow } from 'components/Card'; import { DetailList, Detail, UserDateDetail } from 'components/DetailList'; @@ -50,9 +57,23 @@ function InventoryDetail({ inventory }) { const { organization, user_capabilities: userCapabilities } = inventory.summary_fields; + const { prevent_instance_group_fallback } = inventory; + const deleteDetailsRequests = relatedResourceDeleteRequests.inventory(inventory); + const renderOptionsField = prevent_instance_group_fallback; + + const renderOptions = ( + <TextList component={TextListVariants.ul}> + {prevent_instance_group_fallback && ( + <TextListItem component={TextListItemVariants.li}> + {t`Prevent Instance Group Fallback`} + </TextListItem> + )} + </TextList> + ); + if (isLoading) { return <ContentLoading />; } @@ -104,6 +125,22 @@ function InventoryDetail({ inventory }) { isEmpty={instanceGroups.length === 0} /> )} + {prevent_instance_group_fallback && ( + <Detail + label={t`Prevent Instance Group Fallback`} + dataCy="inv-detail-prevent-instnace-group-fallback" + helpText={helpText.preventInstanceGroupFallback} + /> + )} + {renderOptionsField && ( + <Detail + fullWidth + label={t`Enabled Options`} + value={renderOptions} + dataCy="jt-detail-enabled-options" + helpText={helpText.enabledOptions} + /> + )} {inventory.summary_fields.labels && ( <Detail fullWidth diff --git a/awx/ui/src/screens/Inventory/shared/Inventory.helptext.js b/awx/ui/src/screens/Inventory/shared/Inventory.helptext.js index 19636128f0..685a47f01a 100644 --- a/awx/ui/src/screens/Inventory/shared/Inventory.helptext.js +++ b/awx/ui/src/screens/Inventory/shared/Inventory.helptext.js @@ -191,6 +191,11 @@ const getInventoryHelpTextStrings = () => ({ sourcePath: t`The inventory file to be synced by this source. You can select from the dropdown or enter a file within the input.`, + preventInstanceGroupFallback: t`If enabled, the inventory will prevent adding any organization instance groups to the list of preferred instances groups to run associated job templates on. + Note: If this setting is enabled and you provided an empty list, the global instance groups will be applied.`, + enabledOptions: ( + <p>{t`Prevent Instance Group Fallback: If enabled, the inventory will prevent adding any organization instance groups to the list of preferred instances groups to run associated job templates on.`}</p> + ), }); export default getInventoryHelpTextStrings; diff --git a/awx/ui/src/screens/Inventory/shared/InventoryForm.js b/awx/ui/src/screens/Inventory/shared/InventoryForm.js index e29ffcdfa6..49c91fa73b 100644 --- a/awx/ui/src/screens/Inventory/shared/InventoryForm.js +++ b/awx/ui/src/screens/Inventory/shared/InventoryForm.js @@ -5,14 +5,21 @@ import { func, shape } from 'prop-types'; import { Form, FormGroup } from '@patternfly/react-core'; import { VariablesField } from 'components/CodeEditor'; import Popover from 'components/Popover'; -import FormField, { FormSubmitError } from 'components/FormField'; +import FormField, { + CheckboxField, + FormSubmitError, +} from 'components/FormField'; import FormActionGroup from 'components/FormActionGroup'; import { required } from 'util/validators'; import LabelSelect from 'components/LabelSelect'; import InstanceGroupsLookup from 'components/Lookup/InstanceGroupsLookup'; import OrganizationLookup from 'components/Lookup/OrganizationLookup'; import ContentError from 'components/ContentError'; -import { FormColumnLayout, FormFullWidthLayout } from 'components/FormLayout'; +import { + FormColumnLayout, + FormFullWidthLayout, + FormCheckboxLayout, +} from 'components/FormLayout'; import getHelpText from './Inventory.helptext'; function InventoryFormFields({ inventory }) { @@ -84,6 +91,16 @@ function InventoryFormFields({ inventory }) { createText={t`Create`} /> </FormGroup> + <FormGroup fieldId="inventory-option-checkboxes" label={t`Options`}> + <FormCheckboxLayout> + <CheckboxField + id="option-prevent-instance-group-fallback" + name="prevent_instance_group_fallback" + label={t`Prevent Instance Group Fallback`} + tooltip={helpText.preventInstanceGroupFallback} + /> + </FormCheckboxLayout> + </FormGroup> <VariablesField tooltip={helpText.variables()} id="inventory-variables" @@ -112,6 +129,8 @@ function InventoryForm({ null, instanceGroups: instanceGroups || [], labels: inventory?.summary_fields?.labels?.results || [], + prevent_instance_group_fallback: + inventory.prevent_instance_group_fallback || false, }; return ( <Formik diff --git a/awx/ui/src/screens/Inventory/shared/data.inventory.json b/awx/ui/src/screens/Inventory/shared/data.inventory.json index 12da00ee53..9c6a8eba94 100644 --- a/awx/ui/src/screens/Inventory/shared/data.inventory.json +++ b/awx/ui/src/screens/Inventory/shared/data.inventory.json @@ -3,77 +3,77 @@ "type": "inventory", "url": "/api/v2/inventories/1/", "related": { - "named_url": "/api/v2/inventories/Mike's Inventory++Default/", - "created_by": "/api/v2/users/1/", - "modified_by": "/api/v2/users/1/", - "hosts": "/api/v2/inventories/1/hosts/", - "groups": "/api/v2/inventories/1/groups/", - "root_groups": "/api/v2/inventories/1/root_groups/", - "variable_data": "/api/v2/inventories/1/variable_data/", - "script": "/api/v2/inventories/1/script/", - "tree": "/api/v2/inventories/1/tree/", - "inventory_sources": "/api/v2/inventories/1/inventory_sources/", - "update_inventory_sources": "/api/v2/inventories/1/update_inventory_sources/", - "activity_stream": "/api/v2/inventories/1/activity_stream/", - "job_templates": "/api/v2/inventories/1/job_templates/", - "ad_hoc_commands": "/api/v2/inventories/1/ad_hoc_commands/", - "access_list": "/api/v2/inventories/1/access_list/", - "object_roles": "/api/v2/inventories/1/object_roles/", - "instance_groups": "/api/v2/inventories/1/instance_groups/", - "copy": "/api/v2/inventories/1/copy/", - "organization": "/api/v2/organizations/1/" + "named_url": "/api/v2/inventories/Mike's Inventory++Default/", + "created_by": "/api/v2/users/1/", + "modified_by": "/api/v2/users/1/", + "hosts": "/api/v2/inventories/1/hosts/", + "groups": "/api/v2/inventories/1/groups/", + "root_groups": "/api/v2/inventories/1/root_groups/", + "variable_data": "/api/v2/inventories/1/variable_data/", + "script": "/api/v2/inventories/1/script/", + "tree": "/api/v2/inventories/1/tree/", + "inventory_sources": "/api/v2/inventories/1/inventory_sources/", + "update_inventory_sources": "/api/v2/inventories/1/update_inventory_sources/", + "activity_stream": "/api/v2/inventories/1/activity_stream/", + "job_templates": "/api/v2/inventories/1/job_templates/", + "ad_hoc_commands": "/api/v2/inventories/1/ad_hoc_commands/", + "access_list": "/api/v2/inventories/1/access_list/", + "object_roles": "/api/v2/inventories/1/object_roles/", + "instance_groups": "/api/v2/inventories/1/instance_groups/", + "copy": "/api/v2/inventories/1/copy/", + "organization": "/api/v2/organizations/1/" }, "summary_fields": { - "organization": { - "id": 1, - "name": "Default", - "description": "" + "organization": { + "id": 1, + "name": "Default", + "description": "" + }, + "created_by": { + "id": 1, + "username": "admin", + "first_name": "", + "last_name": "" + }, + "modified_by": { + "id": 1, + "username": "admin", + "first_name": "", + "last_name": "" + }, + "object_roles": { + "admin_role": { + "description": "Can manage all aspects of the inventory", + "name": "Admin", + "id": 19 }, - "created_by": { - "id": 1, - "username": "admin", - "first_name": "", - "last_name": "" + "update_role": { + "description": "May update the inventory", + "name": "Update", + "id": 20 }, - "modified_by": { - "id": 1, - "username": "admin", - "first_name": "", - "last_name": "" + "adhoc_role": { + "description": "May run ad hoc commands on the inventory", + "name": "Ad Hoc", + "id": 21 }, - "object_roles": { - "admin_role": { - "description": "Can manage all aspects of the inventory", - "name": "Admin", - "id": 19 - }, - "update_role": { - "description": "May update the inventory", - "name": "Update", - "id": 20 - }, - "adhoc_role": { - "description": "May run ad hoc commands on the inventory", - "name": "Ad Hoc", - "id": 21 - }, - "use_role": { - "description": "Can use the inventory in a job template", - "name": "Use", - "id": 22 - }, - "read_role": { - "description": "May view settings for the inventory", - "name": "Read", - "id": 23 - } + "use_role": { + "description": "Can use the inventory in a job template", + "name": "Use", + "id": 22 }, - "user_capabilities": { - "edit": true, - "delete": true, - "copy": true, - "adhoc": true + "read_role": { + "description": "May view settings for the inventory", + "name": "Read", + "id": 23 } + }, + "user_capabilities": { + "edit": true, + "delete": true, + "copy": true, + "adhoc": true + } }, "created": "2019-10-04T14:28:04.765571Z", "modified": "2019-10-04T14:28:04.765594Z", @@ -91,5 +91,6 @@ "has_inventory_sources": false, "total_inventory_sources": 0, "inventory_sources_with_failures": 0, - "pending_deletion": false -}
\ No newline at end of file + "pending_deletion": false, + "prevent_instance_group_fallback": false +} diff --git a/awx/ui/src/screens/Template/JobTemplateAdd/JobTemplateAdd.test.js b/awx/ui/src/screens/Template/JobTemplateAdd/JobTemplateAdd.test.js index 9fffccbb33..3fd63394dc 100644 --- a/awx/ui/src/screens/Template/JobTemplateAdd/JobTemplateAdd.test.js +++ b/awx/ui/src/screens/Template/JobTemplateAdd/JobTemplateAdd.test.js @@ -55,6 +55,7 @@ const jobTemplateData = { limit: '', name: '', playbook: '', + prevent_instance_group_fallback: false, project: { id: 1, summary_fields: { organization: { id: 1 } } }, scm_branch: '', skip_tags: '', diff --git a/awx/ui/src/screens/Template/JobTemplateDetail/JobTemplateDetail.js b/awx/ui/src/screens/Template/JobTemplateDetail/JobTemplateDetail.js index 22506185d6..dd5fdc837a 100644 --- a/awx/ui/src/screens/Template/JobTemplateDetail/JobTemplateDetail.js +++ b/awx/ui/src/screens/Template/JobTemplateDetail/JobTemplateDetail.js @@ -63,6 +63,7 @@ function JobTemplateDetail({ template }) { webhook_service, related: { webhook_receiver }, webhook_key, + prevent_instance_group_fallback, custom_virtualenv, } = template; const { id: templateId } = useParams(); @@ -111,7 +112,8 @@ function JobTemplateDetail({ template }) { host_config_key || allow_simultaneous || use_fact_cache || - webhook_service; + webhook_service || + prevent_instance_group_fallback; const renderOptions = ( <TextList component={TextListVariants.ul}> @@ -140,6 +142,11 @@ function JobTemplateDetail({ template }) { {t`Webhooks`} </TextListItem> )} + {prevent_instance_group_fallback && ( + <TextListItem component={TextListItemVariants.li}> + {t`Prevent Instance Group Fallback`} + </TextListItem> + )} </TextList> ); @@ -335,6 +342,13 @@ function JobTemplateDetail({ template }) { } /> )} + {prevent_instance_group_fallback && ( + <Detail + label={t`Prevent Instance Group Fallback`} + dataCy="jt-detail-prevent-instnace-group-fallback" + helpText={helpText.preventInstanceGroupFallback} + /> + )} <UserDateDetail label={t`Created`} date={created} diff --git a/awx/ui/src/screens/Template/JobTemplateEdit/JobTemplateEdit.test.js b/awx/ui/src/screens/Template/JobTemplateEdit/JobTemplateEdit.test.js index 2ada0105d5..cb64051cc9 100644 --- a/awx/ui/src/screens/Template/JobTemplateEdit/JobTemplateEdit.test.js +++ b/awx/ui/src/screens/Template/JobTemplateEdit/JobTemplateEdit.test.js @@ -68,6 +68,7 @@ const mockJobTemplate = { limit: '', name: 'Foo', playbook: 'Baz', + prevent_instance_group_fallback: false, project: 3, scm_branch: '', skip_tags: '', diff --git a/awx/ui/src/screens/Template/shared/JobTemplate.helptext.js b/awx/ui/src/screens/Template/shared/JobTemplate.helptext.js index e1b6e589ff..8aa8b48db3 100644 --- a/awx/ui/src/screens/Template/shared/JobTemplate.helptext.js +++ b/awx/ui/src/screens/Template/shared/JobTemplate.helptext.js @@ -30,6 +30,8 @@ const jtHelpTextStrings = () => ({ privilegeEscalation: t`If enabled, run this playbook as an administrator.`, enableWebhook: t`Enable webhook for this template.`, concurrentJobs: t`If enabled, simultaneous runs of this job template will be allowed.`, + preventInstanceGroupFallback: t`If enabled, the job template will prevent adding any inventory or organization instance groups to the list of preferred instances groups to run on. + Note: If this setting is enabled and you provided an empty list, the global instance groups will be applied.`, enableFactStorage: t`If enabled, this will store gathered facts so they can be viewed at the host level. Facts are persisted and injected into the fact cache at runtime.`, enabledOptions: ( <> @@ -38,6 +40,7 @@ const jtHelpTextStrings = () => ({ <p>{t`Privilege escalation: If enabled, run this playbook as an administrator.`}</p> <p>{t`Provisioning callbacks: Enables creation of a provisioning callback URL. Using the URL a host can contact Ansible AWX and request a configuration update using this job template.`}</p> <p>{t`Webhooks: Enable webhook for this template.`}</p> + <p>{t`Prevent Instance Group Fallback: If enabled, the job template will prevent adding any inventory or organization instance groups to the list of preferred instances groups to run on.`}</p> </> ), forks: ( diff --git a/awx/ui/src/screens/Template/shared/JobTemplateForm.js b/awx/ui/src/screens/Template/shared/JobTemplateForm.js index a82aaa8ad3..7621601e9e 100644 --- a/awx/ui/src/screens/Template/shared/JobTemplateForm.js +++ b/awx/ui/src/screens/Template/shared/JobTemplateForm.js @@ -597,6 +597,12 @@ function JobTemplateForm({ label={t`Enable Fact Storage`} tooltip={helpText.enableFactStorage} /> + <CheckboxField + id="option-prevent-instance-group-fallback" + name="prevent_instance_group_fallback" + label={t`Prevent Instance Group Fallback`} + tooltip={helpText.preventInstanceGroupFallback} + /> </FormCheckboxLayout> </FormGroup> </FormFullWidthLayout> @@ -731,6 +737,8 @@ const FormikApp = withFormik({ limit: template.limit || '', name: template.name || '', playbook: template.playbook || '', + prevent_instance_group_fallback: + template.prevent_instance_group_fallback || false, project: summary_fields?.project || projectValues || null, scm_branch: template.scm_branch || '', skip_tags: template.skip_tags || '', diff --git a/awx/ui/src/screens/Template/shared/data.job_template.json b/awx/ui/src/screens/Template/shared/data.job_template.json index 4d4eb77af1..3c57d0143c 100644 --- a/awx/ui/src/screens/Template/shared/data.job_template.json +++ b/awx/ui/src/screens/Template/shared/data.job_template.json @@ -1,194 +1,199 @@ { - "id": 7, - "type": "job_template", - "url": "/api/v2/job_templates/7/", - "related": { - "named_url": "/api/v2/job_templates/Mike's JT/", - "created_by": "/api/v2/users/1/", - "modified_by": "/api/v2/users/1/", - "labels": "/api/v2/job_templates/7/labels/", - "inventory": "/api/v2/inventories/1/", - "project": "/api/v2/projects/6/", - "credentials": "/api/v2/job_templates/7/credentials/", - "last_job": "/api/v2/jobs/12/", - "jobs": "/api/v2/job_templates/7/jobs/", - "schedules": "/api/v2/job_templates/7/schedules/", - "activity_stream": "/api/v2/job_templates/7/activity_stream/", - "launch": "/api/v2/job_templates/7/launch/", - "notification_templates_started": "/api/v2/job_templates/7/notification_templates_started/", - "notification_templates_success": "/api/v2/job_templates/7/notification_templates_success/", - "notification_templates_error": "/api/v2/job_templates/7/notification_templates_error/", - "access_list": "/api/v2/job_templates/7/access_list/", - "survey_spec": "/api/v2/job_templates/7/survey_spec/", - "object_roles": "/api/v2/job_templates/7/object_roles/", - "instance_groups": "/api/v2/job_templates/7/instance_groups/", - "slice_workflow_jobs": "/api/v2/job_templates/7/slice_workflow_jobs/", - "copy": "/api/v2/job_templates/7/copy/", - "webhook_receiver": "/api/v2/job_templates/7/github/", - "webhook_key": "/api/v2/job_templates/7/webhook_key/" + "id": 7, + "type": "job_template", + "url": "/api/v2/job_templates/7/", + "related": { + "named_url": "/api/v2/job_templates/Mike's JT/", + "created_by": "/api/v2/users/1/", + "modified_by": "/api/v2/users/1/", + "labels": "/api/v2/job_templates/7/labels/", + "inventory": "/api/v2/inventories/1/", + "project": "/api/v2/projects/6/", + "credentials": "/api/v2/job_templates/7/credentials/", + "last_job": "/api/v2/jobs/12/", + "jobs": "/api/v2/job_templates/7/jobs/", + "schedules": "/api/v2/job_templates/7/schedules/", + "activity_stream": "/api/v2/job_templates/7/activity_stream/", + "launch": "/api/v2/job_templates/7/launch/", + "notification_templates_started": "/api/v2/job_templates/7/notification_templates_started/", + "notification_templates_success": "/api/v2/job_templates/7/notification_templates_success/", + "notification_templates_error": "/api/v2/job_templates/7/notification_templates_error/", + "access_list": "/api/v2/job_templates/7/access_list/", + "survey_spec": "/api/v2/job_templates/7/survey_spec/", + "object_roles": "/api/v2/job_templates/7/object_roles/", + "instance_groups": "/api/v2/job_templates/7/instance_groups/", + "slice_workflow_jobs": "/api/v2/job_templates/7/slice_workflow_jobs/", + "copy": "/api/v2/job_templates/7/copy/", + "webhook_receiver": "/api/v2/job_templates/7/github/", + "webhook_key": "/api/v2/job_templates/7/webhook_key/" + }, + "summary_fields": { + "inventory": { + "id": 1, + "name": "Mike's Inventory", + "description": "", + "has_active_failures": false, + "total_hosts": 1, + "hosts_with_active_failures": 0, + "total_groups": 0, + "groups_with_active_failures": 0, + "has_inventory_sources": false, + "total_inventory_sources": 0, + "inventory_sources_with_failures": 0, + "organization_id": 1, + "kind": "" }, - "summary_fields": { - "inventory": { - "id": 1, - "name": "Mike's Inventory", - "description": "", - "has_active_failures": false, - "total_hosts": 1, - "hosts_with_active_failures": 0, - "total_groups": 0, - "groups_with_active_failures": 0, - "has_inventory_sources": false, - "total_inventory_sources": 0, - "inventory_sources_with_failures": 0, - "organization_id": 1, - "kind": "" - }, - "project": { - "id": 6, - "name": "Mike's Project", - "description": "", - "status": "successful", - "scm_type": "git" - }, - "last_job": { - "id": 12, - "name": "Mike's JT", - "description": "", - "finished": "2019-10-01T14:34:35.142483Z", - "status": "successful", - "failed": false - }, - "last_update": { - "id": 12, - "name": "Mike's JT", - "description": "", - "status": "successful", - "failed": false - }, - "created_by": { - "id": 1, - "username": "admin", - "first_name": "", - "last_name": "" - }, - "modified_by": { - "id": 1, - "username": "admin", - "first_name": "", - "last_name": "" - }, - "object_roles": { - "admin_role": { - "description": "Can manage all aspects of the job template", - "name": "Admin", - "id": 24 - }, - "execute_role": { - "description": "May run the job template", - "name": "Execute", - "id": 25 - }, - "read_role": { - "description": "May view settings for the job template", - "name": "Read", - "id": 26 - } - }, - "user_capabilities": { - "edit": true, - "delete": true, - "start": true, - "schedule": true, - "copy": true - }, - "labels": { - "count": 1, - "results": [{ - "id": 91, - "name": "L_91o2" - }] - }, - "survey": { - "title": "", - "description": "" - }, - "recent_jobs": [{ - "id": 12, - "status": "successful", - "finished": "2019-10-01T14:34:35.142483Z", - "type": "job" - }], - "credentials": [{ - "id": 1, - "kind": "ssh", - "name": "Credential 1" - }, - { - "id": 2, - "kind": "awx", - "name": "Credential 2" - } - ], - "webhook_credential": { - "id": "1", - "name": "Webhook Credential" - - }, - "execution_environment": { - "id": 1, - "name": "Default EE", - "description": "", - "image": "quay.io/ansible/awx-ee" - }, - "resolved_environment": { - "id": 1, - "name": "Default EE", - "description": "", - "image": "quay.io/ansible/awx-ee" + "project": { + "id": 6, + "name": "Mike's Project", + "description": "", + "status": "successful", + "scm_type": "git" + }, + "last_job": { + "id": 12, + "name": "Mike's JT", + "description": "", + "finished": "2019-10-01T14:34:35.142483Z", + "status": "successful", + "failed": false + }, + "last_update": { + "id": 12, + "name": "Mike's JT", + "description": "", + "status": "successful", + "failed": false + }, + "created_by": { + "id": 1, + "username": "admin", + "first_name": "", + "last_name": "" + }, + "modified_by": { + "id": 1, + "username": "admin", + "first_name": "", + "last_name": "" + }, + "object_roles": { + "admin_role": { + "description": "Can manage all aspects of the job template", + "name": "Admin", + "id": 24 + }, + "execute_role": { + "description": "May run the job template", + "name": "Execute", + "id": 25 + }, + "read_role": { + "description": "May view settings for the job template", + "name": "Read", + "id": 26 + } + }, + "user_capabilities": { + "edit": true, + "delete": true, + "start": true, + "schedule": true, + "copy": true + }, + "labels": { + "count": 1, + "results": [ + { + "id": 91, + "name": "L_91o2" } + ] + }, + "survey": { + "title": "", + "description": "" + }, + "recent_jobs": [ + { + "id": 12, + "status": "successful", + "finished": "2019-10-01T14:34:35.142483Z", + "type": "job" + } + ], + "credentials": [ + { + "id": 1, + "kind": "ssh", + "name": "Credential 1" + }, + { + "id": 2, + "kind": "awx", + "name": "Credential 2" + } + ], + "webhook_credential": { + "id": "1", + "name": "Webhook Credential" + }, + "execution_environment": { + "id": 1, + "name": "Default EE", + "description": "", + "image": "quay.io/ansible/awx-ee" }, - "created": "2019-09-30T16:18:34.564820Z", - "modified": "2019-10-01T14:47:31.818431Z", - "name": "Mike's JT", - "description": "", - "job_type": "run", - "inventory": 1, - "project": 6, - "playbook": "ping.yml", - "scm_branch": "Foo branch", - "forks": 0, - "limit": "", - "verbosity": 0, - "extra_vars": "", - "job_tags": "T_100,T_200", - "force_handlers": false, - "skip_tags": "S_100,S_200", - "start_at_task": "", - "timeout": 0, - "use_fact_cache": true, - "last_job_run": "2019-10-01T14:34:35.142483Z", - "last_job_failed": false, - "next_job_run": null, - "status": "successful", - "host_config_key": "", - "ask_scm_branch_on_launch": false, - "ask_diff_mode_on_launch": false, - "ask_variables_on_launch": false, - "ask_limit_on_launch": false, - "ask_tags_on_launch": false, - "ask_skip_tags_on_launch": false, - "ask_job_type_on_launch": false, - "ask_verbosity_on_launch": false, - "ask_inventory_on_launch": false, - "ask_credential_on_launch": false, - "survey_enabled": true, - "become_enabled": false, - "diff_mode": false, - "allow_simultaneous": false, - "custom_virtualenv": null, - "job_slice_count": 1, - "webhook_credential": 1, - "webhook_key": "asertdyuhjkhgfd234567kjgfds", - "webhook_service": "github", - "execution_environment": 1 + "resolved_environment": { + "id": 1, + "name": "Default EE", + "description": "", + "image": "quay.io/ansible/awx-ee" + } + }, + "created": "2019-09-30T16:18:34.564820Z", + "modified": "2019-10-01T14:47:31.818431Z", + "name": "Mike's JT", + "description": "", + "job_type": "run", + "inventory": 1, + "project": 6, + "playbook": "ping.yml", + "scm_branch": "Foo branch", + "forks": 0, + "limit": "", + "verbosity": 0, + "extra_vars": "", + "job_tags": "T_100,T_200", + "force_handlers": false, + "skip_tags": "S_100,S_200", + "start_at_task": "", + "timeout": 0, + "use_fact_cache": true, + "last_job_run": "2019-10-01T14:34:35.142483Z", + "last_job_failed": false, + "next_job_run": null, + "status": "successful", + "host_config_key": "", + "ask_scm_branch_on_launch": false, + "ask_diff_mode_on_launch": false, + "ask_variables_on_launch": false, + "ask_limit_on_launch": false, + "ask_tags_on_launch": false, + "ask_skip_tags_on_launch": false, + "ask_job_type_on_launch": false, + "ask_verbosity_on_launch": false, + "ask_inventory_on_launch": false, + "ask_credential_on_launch": false, + "survey_enabled": true, + "become_enabled": false, + "diff_mode": false, + "allow_simultaneous": false, + "custom_virtualenv": null, + "job_slice_count": 1, + "webhook_credential": 1, + "webhook_key": "asertdyuhjkhgfd234567kjgfds", + "webhook_service": "github", + "execution_environment": 1, + "prevent_instance_group_fallback": false } diff --git a/awx_collection/plugins/modules/inventory.py b/awx_collection/plugins/modules/inventory.py index f04e94c533..b4784f5260 100644 --- a/awx_collection/plugins/modules/inventory.py +++ b/awx_collection/plugins/modules/inventory.py @@ -66,6 +66,10 @@ options: - list of Instance Groups for this Organization to run on. type: list elements: str + prevent_instance_group_fallback: + description: + - Prevent falling back to instance groups set on the organization + type: bool state: description: - Desired state of the resource. @@ -111,6 +115,7 @@ def main(): kind=dict(choices=['', 'smart'], default=''), host_filter=dict(), instance_groups=dict(type="list", elements='str'), + prevent_instance_group_fallback=dict(type='bool'), state=dict(choices=['present', 'absent'], default='present'), ) @@ -127,6 +132,7 @@ def main(): state = module.params.get('state') kind = module.params.get('kind') host_filter = module.params.get('host_filter') + prevent_instance_group_fallback = module.params.get('prevent_instance_group_fallback') # Attempt to look up the related items the user specified (these will fail the module if not found) org_id = module.resolve_name_to_id('organizations', organization) @@ -157,6 +163,8 @@ def main(): 'kind': kind, 'host_filter': host_filter, } + if prevent_instance_group_fallback is not None: + inventory_fields['prevent_instance_group_fallback'] = prevent_instance_group_fallback if description is not None: inventory_fields['description'] = description if variables is not None: diff --git a/awx_collection/plugins/modules/job_template.py b/awx_collection/plugins/modules/job_template.py index 5a7e9b6e25..2a8408e2a4 100644 --- a/awx_collection/plugins/modules/job_template.py +++ b/awx_collection/plugins/modules/job_template.py @@ -315,6 +315,10 @@ options: - list of notifications to send on error type: list elements: str + prevent_instance_group_fallback: + description: + - Prevent falling back to instance groups set on the associated inventory or organization + type: bool extends_documentation_fragment: awx.awx.auth @@ -441,6 +445,7 @@ def main(): notification_templates_started=dict(type="list", elements='str'), notification_templates_success=dict(type="list", elements='str'), notification_templates_error=dict(type="list", elements='str'), + prevent_instance_group_fallback=dict(type="bool"), state=dict(choices=['present', 'absent'], default='present'), ) @@ -539,6 +544,7 @@ def main(): 'custom_virtualenv', 'job_slice_count', 'webhook_service', + 'prevent_instance_group_fallback', ): field_val = module.params.get(field_name) if field_val is not None: diff --git a/awxkit/awxkit/api/pages/inventory.py b/awxkit/awxkit/api/pages/inventory.py index 33c84f2aa2..8431f15965 100644 --- a/awxkit/awxkit/api/pages/inventory.py +++ b/awxkit/awxkit/api/pages/inventory.py @@ -59,7 +59,7 @@ class Inventory(HasCopy, HasCreate, HasInstanceGroups, HasVariables, base.Base): organization=organization.id, ) - optional_fields = ('host_filter', 'kind', 'variables') + optional_fields = ('host_filter', 'kind', 'variables', 'prevent_instance_group_fallback') update_payload(payload, optional_fields, kwargs) diff --git a/awxkit/awxkit/api/pages/job_templates.py b/awxkit/awxkit/api/pages/job_templates.py index 46862d9f2b..5378059843 100644 --- a/awxkit/awxkit/api/pages/job_templates.py +++ b/awxkit/awxkit/api/pages/job_templates.py @@ -79,6 +79,7 @@ class JobTemplate(HasCopy, HasCreate, HasInstanceGroups, HasNotifications, HasSu 'webhook_service', 'webhook_credential', 'scm_branch', + 'prevent_instance_group_fallback', ) update_payload(payload, optional_fields, kwargs) |