diff --git a/awx/api/serializers.py b/awx/api/serializers.py index 5c3edddb52..b466d1e91d 100644 --- a/awx/api/serializers.py +++ b/awx/api/serializers.py @@ -2073,7 +2073,7 @@ class InventorySourceSerializer(UnifiedJobTemplateSerializer, InventorySourceOpt class Meta: model = InventorySource - fields = ('*', 'name', 'inventory', 'update_on_launch', 'update_cache_timeout', 'source_project', 'update_on_project_update') + ( + fields = ('*', 'name', 'inventory', 'update_on_launch', 'update_cache_timeout', 'source_project') + ( 'last_update_failed', 'last_updated', ) # Backwards compatibility. @@ -2136,11 +2136,6 @@ class InventorySourceSerializer(UnifiedJobTemplateSerializer, InventorySourceOpt raise serializers.ValidationError(_("Cannot use manual project for SCM-based inventory.")) return value - def validate_update_on_project_update(self, value): - if value and self.instance and self.instance.schedules.exists(): - raise serializers.ValidationError(_("Setting not compatible with existing schedules.")) - return value - def validate_inventory(self, value): if value and value.kind == 'smart': raise serializers.ValidationError({"detail": _("Cannot create Inventory Source for Smart Inventory")}) @@ -2191,7 +2186,7 @@ class InventorySourceSerializer(UnifiedJobTemplateSerializer, InventorySourceOpt if ('source' in attrs or 'source_project' in attrs) and get_field_from_model_or_attrs('source_project') is None: raise serializers.ValidationError({"source_project": _("Project required for scm type sources.")}) else: - redundant_scm_fields = list(filter(lambda x: attrs.get(x, None), ['source_project', 'source_path', 'update_on_project_update'])) + redundant_scm_fields = list(filter(lambda x: attrs.get(x, None), ['source_project', 'source_path'])) if redundant_scm_fields: raise serializers.ValidationError({"detail": _("Cannot set %s if not SCM type." % ' '.join(redundant_scm_fields))}) @@ -4745,13 +4740,6 @@ class ScheduleSerializer(LaunchConfigurationBaseSerializer, SchedulePreviewSeria raise serializers.ValidationError(_('Inventory Source must be a cloud resource.')) elif type(value) == Project and value.scm_type == '': raise serializers.ValidationError(_('Manual Project cannot have a schedule set.')) - elif type(value) == InventorySource and value.source == 'scm' and value.update_on_project_update: - raise serializers.ValidationError( - _( - 'Inventory sources with `update_on_project_update` cannot be scheduled. ' - 'Schedule its source project `{}` instead.'.format(value.source_project.name) - ) - ) return value def validate(self, attrs): diff --git a/awx/api/views/__init__.py b/awx/api/views/__init__.py index 54dcf7034b..0d46c05834 100644 --- a/awx/api/views/__init__.py +++ b/awx/api/views/__init__.py @@ -115,7 +115,6 @@ from awx.api.metadata import RoleMetadata from awx.main.constants import ACTIVE_STATES, SURVEY_TYPE_MAPPING from awx.main.scheduler.dag_workflow import WorkflowDAG from awx.api.views.mixin import ( - ControlledByScmMixin, InstanceGroupMembershipMixin, OrganizationCountsMixin, RelatedJobsPreventDeleteMixin, @@ -1675,7 +1674,7 @@ class HostList(HostRelatedSearchMixin, ListCreateAPIView): return Response(dict(error=_(str(e))), status=status.HTTP_400_BAD_REQUEST) -class HostDetail(RelatedJobsPreventDeleteMixin, ControlledByScmMixin, RetrieveUpdateDestroyAPIView): +class HostDetail(RelatedJobsPreventDeleteMixin, RetrieveUpdateDestroyAPIView): always_allow_superuser = False model = models.Host @@ -1709,7 +1708,7 @@ class InventoryHostsList(HostRelatedSearchMixin, SubListCreateAttachDetachAPIVie return qs -class HostGroupsList(ControlledByScmMixin, SubListCreateAttachDetachAPIView): +class HostGroupsList(SubListCreateAttachDetachAPIView): '''the list of groups a host is directly a member of''' model = models.Group @@ -1825,7 +1824,7 @@ class EnforceParentRelationshipMixin(object): return super(EnforceParentRelationshipMixin, self).create(request, *args, **kwargs) -class GroupChildrenList(ControlledByScmMixin, EnforceParentRelationshipMixin, SubListCreateAttachDetachAPIView): +class GroupChildrenList(EnforceParentRelationshipMixin, SubListCreateAttachDetachAPIView): model = models.Group serializer_class = serializers.GroupSerializer @@ -1871,7 +1870,7 @@ class GroupPotentialChildrenList(SubListAPIView): return qs.exclude(pk__in=except_pks) -class GroupHostsList(HostRelatedSearchMixin, ControlledByScmMixin, SubListCreateAttachDetachAPIView): +class GroupHostsList(HostRelatedSearchMixin, SubListCreateAttachDetachAPIView): '''the list of hosts directly below a group''' model = models.Host @@ -1935,7 +1934,7 @@ class GroupActivityStreamList(SubListAPIView): return qs.filter(Q(group=parent) | Q(host__in=parent.hosts.all())) -class GroupDetail(RelatedJobsPreventDeleteMixin, ControlledByScmMixin, RetrieveUpdateDestroyAPIView): +class GroupDetail(RelatedJobsPreventDeleteMixin, RetrieveUpdateDestroyAPIView): model = models.Group serializer_class = serializers.GroupSerializer diff --git a/awx/api/views/inventory.py b/awx/api/views/inventory.py index 43815ae565..65e59790ac 100644 --- a/awx/api/views/inventory.py +++ b/awx/api/views/inventory.py @@ -41,7 +41,7 @@ from awx.api.serializers import ( JobTemplateSerializer, LabelSerializer, ) -from awx.api.views.mixin import RelatedJobsPreventDeleteMixin, ControlledByScmMixin +from awx.api.views.mixin import RelatedJobsPreventDeleteMixin from awx.api.pagination import UnifiedJobEventPagination @@ -75,7 +75,7 @@ class InventoryList(ListCreateAPIView): serializer_class = InventorySerializer -class InventoryDetail(RelatedJobsPreventDeleteMixin, ControlledByScmMixin, RetrieveUpdateDestroyAPIView): +class InventoryDetail(RelatedJobsPreventDeleteMixin, RetrieveUpdateDestroyAPIView): model = Inventory serializer_class = InventorySerializer diff --git a/awx/api/views/mixin.py b/awx/api/views/mixin.py index a0a679d4e5..63762c1634 100644 --- a/awx/api/views/mixin.py +++ b/awx/api/views/mixin.py @@ -10,13 +10,12 @@ from django.shortcuts import get_object_or_404 from django.utils.timezone import now from django.utils.translation import gettext_lazy as _ -from rest_framework.permissions import SAFE_METHODS from rest_framework.exceptions import PermissionDenied from rest_framework.response import Response from rest_framework import status from awx.main.constants import ACTIVE_STATES -from awx.main.utils import get_object_or_400, parse_yaml_or_json +from awx.main.utils import get_object_or_400 from awx.main.models.ha import Instance, InstanceGroup from awx.main.models.organization import Team from awx.main.models.projects import Project @@ -186,35 +185,6 @@ class OrganizationCountsMixin(object): return full_context -class ControlledByScmMixin(object): - """ - Special method to reset SCM inventory commit hash - if anything that it manages changes. - """ - - def _reset_inv_src_rev(self, obj): - if self.request.method in SAFE_METHODS or not obj: - return - project_following_sources = obj.inventory_sources.filter(update_on_project_update=True, source='scm') - if project_following_sources: - # Allow inventory changes unrelated to variables - if self.model == Inventory and ( - not self.request or not self.request.data or parse_yaml_or_json(self.request.data.get('variables', '')) == parse_yaml_or_json(obj.variables) - ): - return - project_following_sources.update(scm_last_revision='') - - def get_object(self): - obj = super(ControlledByScmMixin, self).get_object() - self._reset_inv_src_rev(obj) - return obj - - def get_parent_object(self): - obj = super(ControlledByScmMixin, self).get_parent_object() - self._reset_inv_src_rev(obj) - return obj - - class NoTruncateMixin(object): def get_serializer_context(self): context = super().get_serializer_context() diff --git a/awx/main/migrations/0164_remove_inventorysource_update_on_project_update.py b/awx/main/migrations/0164_remove_inventorysource_update_on_project_update.py new file mode 100644 index 0000000000..59c4bdf839 --- /dev/null +++ b/awx/main/migrations/0164_remove_inventorysource_update_on_project_update.py @@ -0,0 +1,40 @@ +# Generated by Django 3.2.13 on 2022-06-21 21:29 + +from django.db import migrations +import logging + +logger = logging.getLogger("awx") + + +def forwards(apps, schema_editor): + InventorySource = apps.get_model('main', 'InventorySource') + sources = InventorySource.objects.filter(update_on_project_update=True) + for src in sources: + if src.update_on_launch == False: + src.update_on_launch = True + src.save(update_fields=['update_on_launch']) + logger.info(f"Setting update_on_launch to True for {src}") + proj = src.source_project + if proj and proj.scm_update_on_launch is False: + proj.scm_update_on_launch = True + proj.save(update_fields=['scm_update_on_launch']) + logger.warning(f"Setting scm_update_on_launch to True for {proj}") + + +class Migration(migrations.Migration): + + dependencies = [ + ('main', '0163_convert_job_tags_to_textfield'), + ] + + operations = [ + migrations.RunPython(forwards, migrations.RunPython.noop), + migrations.RemoveField( + model_name='inventorysource', + name='scm_last_revision', + ), + migrations.RemoveField( + model_name='inventorysource', + name='update_on_project_update', + ), + ] diff --git a/awx/main/models/inventory.py b/awx/main/models/inventory.py index 14e7477461..9a386db9c2 100644 --- a/awx/main/models/inventory.py +++ b/awx/main/models/inventory.py @@ -985,22 +985,11 @@ class InventorySource(UnifiedJobTemplate, InventorySourceOptions, CustomVirtualE default=None, null=True, ) - scm_last_revision = models.CharField( - max_length=1024, - blank=True, - default='', - editable=False, - ) - update_on_project_update = models.BooleanField( - default=False, - help_text=_( - 'This field is deprecated and will be removed in a future release. ' - 'In future release, functionality will be migrated to source project update_on_launch.' - ), - ) + update_on_launch = models.BooleanField( default=False, ) + update_cache_timeout = models.PositiveIntegerField( default=0, ) @@ -1038,14 +1027,6 @@ class InventorySource(UnifiedJobTemplate, InventorySourceOptions, CustomVirtualE self.name = 'inventory source (%s)' % replace_text if 'name' not in update_fields: update_fields.append('name') - # Reset revision if SCM source has changed parameters - if self.source == 'scm' and not is_new_instance: - before_is = self.__class__.objects.get(pk=self.pk) - if before_is.source_path != self.source_path or before_is.source_project_id != self.source_project_id: - # Reset the scm_revision if file changed to force update - self.scm_last_revision = '' - if 'scm_last_revision' not in update_fields: - update_fields.append('scm_last_revision') # Do the actual save. super(InventorySource, self).save(*args, **kwargs) @@ -1054,10 +1035,6 @@ class InventorySource(UnifiedJobTemplate, InventorySourceOptions, CustomVirtualE if replace_text in self.name: self.name = self.name.replace(replace_text, str(self.pk)) super(InventorySource, self).save(update_fields=['name']) - if self.source == 'scm' and is_new_instance and self.update_on_project_update: - # Schedule a new Project update if one is not already queued - if self.source_project and not self.source_project.project_updates.filter(status__in=['new', 'pending', 'waiting']).exists(): - self.update() if not getattr(_inventory_updates, 'is_updating', False): if self.inventory is not None: self.inventory.update_computed_fields() @@ -1147,25 +1124,6 @@ class InventorySource(UnifiedJobTemplate, InventorySourceOptions, CustomVirtualE ) return dict(error=list(error_notification_templates), started=list(started_notification_templates), success=list(success_notification_templates)) - def clean_update_on_project_update(self): - if ( - self.update_on_project_update is True - and self.source == 'scm' - and InventorySource.objects.filter(Q(inventory=self.inventory, update_on_project_update=True, source='scm') & ~Q(id=self.id)).exists() - ): - raise ValidationError(_("More than one SCM-based inventory source with update on project update per-inventory not allowed.")) - return self.update_on_project_update - - def clean_update_on_launch(self): - if self.update_on_project_update is True and self.source == 'scm' and self.update_on_launch is True: - raise ValidationError( - _( - "Cannot update SCM-based inventory source on launch if set to update on project update. " - "Instead, configure the corresponding source project to update on launch." - ) - ) - return self.update_on_launch - def clean_source_path(self): if self.source != 'scm' and self.source_path: raise ValidationError(_("Cannot set source_path if not SCM type.")) @@ -1301,13 +1259,6 @@ class InventoryUpdate(UnifiedJob, InventorySourceOptions, JobNotificationMixin, return self.global_instance_groups return selected_groups - def cancel(self, job_explanation=None, is_chain=False): - res = super(InventoryUpdate, self).cancel(job_explanation=job_explanation, is_chain=is_chain) - if res: - if self.launch_type != 'scm' and self.source_project_update: - self.source_project_update.cancel(job_explanation=job_explanation) - return res - class CustomInventoryScript(CommonModelNameNotUnique, ResourceMixin): class Meta: diff --git a/awx/main/models/unified_jobs.py b/awx/main/models/unified_jobs.py index 47794a7ef8..8a53fb5a19 100644 --- a/awx/main/models/unified_jobs.py +++ b/awx/main/models/unified_jobs.py @@ -533,7 +533,7 @@ class UnifiedJob( ('workflow', _('Workflow')), # Job was started from a workflow job. ('webhook', _('Webhook')), # Job was started from a webhook event. ('sync', _('Sync')), # Job was started from a project sync. - ('scm', _('SCM Update')), # Job was created as an Inventory SCM sync. + ('scm', _('SCM Update')), # (deprecated) Job was created as an Inventory SCM sync. ] PASSWORD_FIELDS = ('start_args',) diff --git a/awx/main/tasks/jobs.py b/awx/main/tasks/jobs.py index 2963af669c..9c58e1eebe 100644 --- a/awx/main/tasks/jobs.py +++ b/awx/main/tasks/jobs.py @@ -19,7 +19,6 @@ from uuid import uuid4 # Django from django.conf import settings -from django.db import transaction # Runner @@ -34,7 +33,6 @@ from gitdb.exc import BadName as BadGitName from awx.main.dispatch.publish import task from awx.main.dispatch import get_local_queuename from awx.main.constants import ( - ACTIVE_STATES, PRIVILEGE_ESCALATION_METHODS, STANDARD_INVENTORY_UPDATE_ENV, JOB_FOLDER_PREFIX, @@ -1168,64 +1166,6 @@ class RunProjectUpdate(BaseTask): d[r'^Are you sure you want to continue connecting \(yes/no\)\?\s*?$'] = 'yes' return d - def _update_dependent_inventories(self, project_update, dependent_inventory_sources): - scm_revision = project_update.project.scm_revision - inv_update_class = InventoryUpdate._get_task_class() - for inv_src in dependent_inventory_sources: - if not inv_src.update_on_project_update: - continue - if inv_src.scm_last_revision == scm_revision: - logger.debug('Skipping SCM inventory update for `{}` because ' 'project has not changed.'.format(inv_src.name)) - continue - logger.debug('Local dependent inventory update for `{}`.'.format(inv_src.name)) - with transaction.atomic(): - if InventoryUpdate.objects.filter(inventory_source=inv_src, status__in=ACTIVE_STATES).exists(): - logger.debug('Skipping SCM inventory update for `{}` because ' 'another update is already active.'.format(inv_src.name)) - continue - - if settings.IS_K8S: - instance_group = InventoryUpdate(inventory_source=inv_src).preferred_instance_groups[0] - else: - instance_group = project_update.instance_group - - local_inv_update = inv_src.create_inventory_update( - _eager_fields=dict( - launch_type='scm', - status='running', - instance_group=instance_group, - execution_node=project_update.execution_node, - controller_node=project_update.execution_node, - source_project_update=project_update, - celery_task_id=project_update.celery_task_id, - ) - ) - local_inv_update.log_lifecycle("controller_node_chosen") - local_inv_update.log_lifecycle("execution_node_chosen") - try: - create_partition(local_inv_update.event_class._meta.db_table, start=local_inv_update.created) - inv_update_class().run(local_inv_update.id) - except Exception: - logger.exception('{} Unhandled exception updating dependent SCM inventory sources.'.format(project_update.log_format)) - - try: - project_update.refresh_from_db() - except ProjectUpdate.DoesNotExist: - logger.warning('Project update deleted during updates of dependent SCM inventory sources.') - break - try: - local_inv_update.refresh_from_db() - except InventoryUpdate.DoesNotExist: - logger.warning('%s Dependent inventory update deleted during execution.', project_update.log_format) - continue - if project_update.cancel_flag: - logger.info('Project update {} was canceled while updating dependent inventories.'.format(project_update.log_format)) - break - if local_inv_update.cancel_flag: - logger.info('Continuing to process project dependencies after {} was canceled'.format(local_inv_update.log_format)) - if local_inv_update.status == 'successful': - inv_src.scm_last_revision = scm_revision - inv_src.save(update_fields=['scm_last_revision']) - def release_lock(self, instance): try: fcntl.lockf(self.lock_fd, fcntl.LOCK_UN) @@ -1435,12 +1375,6 @@ class RunProjectUpdate(BaseTask): p.inventory_files = p.inventories p.save(update_fields=['scm_revision', 'playbook_files', 'inventory_files']) - # Update any inventories that depend on this project - dependent_inventory_sources = p.scm_inventory_sources.filter(update_on_project_update=True) - if len(dependent_inventory_sources) > 0: - if status == 'successful' and instance.launch_type != 'sync': - self._update_dependent_inventories(instance, dependent_inventory_sources) - def build_execution_environment_params(self, instance, private_data_dir): if settings.IS_K8S: return {} @@ -1620,9 +1554,7 @@ class RunInventoryUpdate(BaseTask): source_project = None if inventory_update.inventory_source: source_project = inventory_update.inventory_source.source_project - if ( - inventory_update.source == 'scm' and inventory_update.launch_type != 'scm' and source_project and source_project.scm_type - ): # never ever update manual projects + if inventory_update.source == 'scm' and source_project and source_project.scm_type: # never ever update manual projects # Check if the content cache exists, so that we do not unnecessarily re-download roles sync_needs = ['update_{}'.format(source_project.scm_type)] @@ -1655,8 +1587,6 @@ class RunInventoryUpdate(BaseTask): sync_task = project_update_task(job_private_data_dir=private_data_dir) sync_task.run(local_project_sync.id) local_project_sync.refresh_from_db() - inventory_update.inventory_source.scm_last_revision = local_project_sync.scm_revision - inventory_update.inventory_source.save(update_fields=['scm_last_revision']) except Exception: inventory_update = self.update_model( inventory_update.pk, @@ -1667,9 +1597,6 @@ class RunInventoryUpdate(BaseTask): ), ) raise - elif inventory_update.source == 'scm' and inventory_update.launch_type == 'scm' and source_project: - # This follows update, not sync, so make copy here - RunProjectUpdate.make_local_copy(source_project, private_data_dir) def post_run_hook(self, inventory_update, status): super(RunInventoryUpdate, self).post_run_hook(inventory_update, status) diff --git a/awx/main/tests/functional/api/test_deprecated_credential_assignment.py b/awx/main/tests/functional/api/test_deprecated_credential_assignment.py index e8b4c2070e..266218c0cb 100644 --- a/awx/main/tests/functional/api/test_deprecated_credential_assignment.py +++ b/awx/main/tests/functional/api/test_deprecated_credential_assignment.py @@ -9,9 +9,7 @@ from awx.api.versioning import reverse @pytest.fixture def ec2_source(inventory, project): with mock.patch('awx.main.models.unified_jobs.UnifiedJobTemplate.update'): - return inventory.inventory_sources.create( - name='some_source', update_on_project_update=True, source='ec2', source_project=project, scm_last_revision=project.scm_revision - ) + return inventory.inventory_sources.create(name='some_source', source='ec2', source_project=project) @pytest.fixture diff --git a/awx/main/tests/functional/api/test_inventory.py b/awx/main/tests/functional/api/test_inventory.py index 5bb80bdbbd..80357a22f9 100644 --- a/awx/main/tests/functional/api/test_inventory.py +++ b/awx/main/tests/functional/api/test_inventory.py @@ -13,9 +13,7 @@ from awx.main.models import InventorySource, Inventory, ActivityStream @pytest.fixture def scm_inventory(inventory, project): with mock.patch('awx.main.models.unified_jobs.UnifiedJobTemplate.update'): - inventory.inventory_sources.create( - name='foobar', update_on_project_update=True, source='scm', source_project=project, scm_last_revision=project.scm_revision - ) + inventory.inventory_sources.create(name='foobar', source='scm', source_project=project) return inventory @@ -23,9 +21,7 @@ def scm_inventory(inventory, project): def factory_scm_inventory(inventory, project): def fn(**kwargs): with mock.patch('awx.main.models.unified_jobs.UnifiedJobTemplate.update'): - return inventory.inventory_sources.create( - source_project=project, overwrite_vars=True, source='scm', scm_last_revision=project.scm_revision, **kwargs - ) + return inventory.inventory_sources.create(source_project=project, overwrite_vars=True, source='scm', **kwargs) return fn @@ -544,15 +540,12 @@ class TestControlledBySCM: def test_safe_method_works(self, get, options, scm_inventory, admin_user): get(scm_inventory.get_absolute_url(), admin_user, expect=200) options(scm_inventory.get_absolute_url(), admin_user, expect=200) - assert InventorySource.objects.get(inventory=scm_inventory.pk).scm_last_revision != '' def test_vars_edit_reset(self, patch, scm_inventory, admin_user): patch(scm_inventory.get_absolute_url(), {'variables': 'hello: world'}, admin_user, expect=200) - assert InventorySource.objects.get(inventory=scm_inventory.pk).scm_last_revision == '' def test_name_edit_allowed(self, patch, scm_inventory, admin_user): patch(scm_inventory.get_absolute_url(), {'variables': '---', 'name': 'newname'}, admin_user, expect=200) - assert InventorySource.objects.get(inventory=scm_inventory.pk).scm_last_revision != '' def test_host_associations_reset(self, post, scm_inventory, admin_user): inv_src = scm_inventory.inventory_sources.first() @@ -560,14 +553,12 @@ class TestControlledBySCM: g = inv_src.groups.create(name='fooland', inventory=scm_inventory) post(reverse('api:host_groups_list', kwargs={'pk': h.id}), {'id': g.id}, admin_user, expect=204) post(reverse('api:group_hosts_list', kwargs={'pk': g.id}), {'id': h.id}, admin_user, expect=204) - assert InventorySource.objects.get(inventory=scm_inventory.pk).scm_last_revision == '' def test_group_group_associations_reset(self, post, scm_inventory, admin_user): inv_src = scm_inventory.inventory_sources.first() g1 = inv_src.groups.create(name='barland', inventory=scm_inventory) g2 = inv_src.groups.create(name='fooland', inventory=scm_inventory) post(reverse('api:group_children_list', kwargs={'pk': g1.id}), {'id': g2.id}, admin_user, expect=204) - assert InventorySource.objects.get(inventory=scm_inventory.pk).scm_last_revision == '' def test_host_group_delete_reset(self, delete, scm_inventory, admin_user): inv_src = scm_inventory.inventory_sources.first() @@ -575,7 +566,6 @@ class TestControlledBySCM: g = inv_src.groups.create(name='fooland', inventory=scm_inventory) delete(h.get_absolute_url(), admin_user, expect=204) delete(g.get_absolute_url(), admin_user, expect=204) - assert InventorySource.objects.get(inventory=scm_inventory.pk).scm_last_revision == '' def test_remove_scm_inv_src(self, delete, scm_inventory, admin_user): inv_src = scm_inventory.inventory_sources.first() @@ -588,7 +578,6 @@ class TestControlledBySCM: { 'name': 'new inv src', 'source_project': project.pk, - 'update_on_project_update': False, 'source': 'scm', 'overwrite_vars': True, 'source_vars': 'plugin: a.b.c', @@ -597,27 +586,6 @@ class TestControlledBySCM: expect=201, ) - def test_adding_inv_src_prohibited(self, post, scm_inventory, project, admin_user): - post( - reverse('api:inventory_inventory_sources_list', kwargs={'pk': scm_inventory.id}), - {'name': 'new inv src', 'source_project': project.pk, 'update_on_project_update': True, 'source': 'scm', 'overwrite_vars': True}, - admin_user, - expect=400, - ) - - def test_two_update_on_project_update_inv_src_prohibited(self, patch, scm_inventory, factory_scm_inventory, project, admin_user): - scm_inventory2 = factory_scm_inventory(name="scm_inventory2") - res = patch( - reverse('api:inventory_source_detail', kwargs={'pk': scm_inventory2.id}), - { - 'update_on_project_update': True, - }, - admin_user, - expect=400, - ) - content = json.loads(res.content) - assert content['update_on_project_update'] == ["More than one SCM-based inventory source with update on project update " "per-inventory not allowed."] - def test_adding_inv_src_without_proj_access_prohibited(self, post, project, inventory, rando): inventory.admin_role.members.add(rando) post( diff --git a/awx/main/tests/functional/conftest.py b/awx/main/tests/functional/conftest.py index ea18b491e6..2e3563a2b6 100644 --- a/awx/main/tests/functional/conftest.py +++ b/awx/main/tests/functional/conftest.py @@ -347,9 +347,7 @@ def scm_inventory_source(inventory, project): source_project=project, source='scm', source_path='inventory_file', - update_on_project_update=True, inventory=inventory, - scm_last_revision=project.scm_revision, ) with mock.patch('awx.main.models.unified_jobs.UnifiedJobTemplate.update'): inv_src.save() diff --git a/awx/main/tests/functional/models/test_inventory.py b/awx/main/tests/functional/models/test_inventory.py index 6c418e5b16..d246853c83 100644 --- a/awx/main/tests/functional/models/test_inventory.py +++ b/awx/main/tests/functional/models/test_inventory.py @@ -3,8 +3,6 @@ import pytest from unittest import mock -from django.core.exceptions import ValidationError - # AWX from awx.main.models import Host, Inventory, InventorySource, InventoryUpdate, CredentialType, Credential, Job from awx.main.constants import CLOUD_PROVIDERS @@ -123,19 +121,6 @@ class TestActiveCount: @pytest.mark.django_db class TestSCMUpdateFeatures: - def test_automatic_project_update_on_create(self, inventory, project): - inv_src = InventorySource(source_project=project, source_path='inventory_file', inventory=inventory, update_on_project_update=True, source='scm') - with mock.patch.object(inv_src, 'update') as mck_update: - inv_src.save() - mck_update.assert_called_once_with() - - def test_reset_scm_revision(self, scm_inventory_source): - starting_rev = scm_inventory_source.scm_last_revision - assert starting_rev != '' - scm_inventory_source.source_path = '/newfolder/newfile.ini' - scm_inventory_source.save() - assert scm_inventory_source.scm_last_revision == '' - def test_source_location(self, scm_inventory_source): # Combines project directory with the inventory file specified inventory_update = InventoryUpdate(inventory_source=scm_inventory_source, source_path=scm_inventory_source.source_path) @@ -167,22 +152,6 @@ class TestRelatedJobs: assert job.id in [jerb.id for jerb in group._get_related_jobs()] -@pytest.mark.django_db -class TestSCMClean: - def test_clean_update_on_project_update_multiple(self, inventory): - inv_src1 = InventorySource(inventory=inventory, update_on_project_update=True, source='scm') - inv_src1.clean_update_on_project_update() - inv_src1.save() - - inv_src1.source_vars = '---\nhello: world' - inv_src1.clean_update_on_project_update() - - inv_src2 = InventorySource(inventory=inventory, update_on_project_update=True, source='scm') - - with pytest.raises(ValidationError): - inv_src2.clean_update_on_project_update() - - @pytest.mark.django_db class TestInventorySourceInjectors: def test_extra_credentials(self, project, credential): diff --git a/awx/main/tests/functional/test_tasks.py b/awx/main/tests/functional/test_tasks.py index cb66a07ba3..0e85dc389d 100644 --- a/awx/main/tests/functional/test_tasks.py +++ b/awx/main/tests/functional/test_tasks.py @@ -4,9 +4,8 @@ import os import tempfile import shutil -from awx.main.tasks.jobs import RunProjectUpdate, RunInventoryUpdate from awx.main.tasks.system import execution_node_health_check, _cleanup_images_and_files -from awx.main.models import ProjectUpdate, InventoryUpdate, InventorySource, Instance, Job +from awx.main.models import Instance, Job @pytest.fixture @@ -27,63 +26,6 @@ def test_no_worker_info_on_AWX_nodes(node_type): execution_node_health_check(hostname) -@pytest.mark.django_db -class TestDependentInventoryUpdate: - def test_dependent_inventory_updates_is_called(self, scm_inventory_source, scm_revision_file, mock_me): - task = RunProjectUpdate() - task.revision_path = scm_revision_file - proj_update = scm_inventory_source.source_project.create_project_update() - with mock.patch.object(RunProjectUpdate, '_update_dependent_inventories') as inv_update_mck: - with mock.patch.object(RunProjectUpdate, 'release_lock'): - task.post_run_hook(proj_update, 'successful') - inv_update_mck.assert_called_once_with(proj_update, mock.ANY) - - def test_no_unwanted_dependent_inventory_updates(self, project, scm_revision_file, mock_me): - task = RunProjectUpdate() - task.revision_path = scm_revision_file - proj_update = project.create_project_update() - with mock.patch.object(RunProjectUpdate, '_update_dependent_inventories') as inv_update_mck: - with mock.patch.object(RunProjectUpdate, 'release_lock'): - task.post_run_hook(proj_update, 'successful') - assert not inv_update_mck.called - - def test_dependent_inventory_updates(self, scm_inventory_source, default_instance_group, mock_me): - task = RunProjectUpdate() - scm_inventory_source.scm_last_revision = '' - proj_update = ProjectUpdate.objects.create(project=scm_inventory_source.source_project) - with mock.patch.object(RunInventoryUpdate, 'run') as iu_run_mock: - with mock.patch('awx.main.tasks.jobs.create_partition'): - task._update_dependent_inventories(proj_update, [scm_inventory_source]) - assert InventoryUpdate.objects.count() == 1 - inv_update = InventoryUpdate.objects.first() - iu_run_mock.assert_called_once_with(inv_update.id) - assert inv_update.source_project_update_id == proj_update.pk - - def test_dependent_inventory_project_cancel(self, project, inventory, default_instance_group, mock_me): - """ - Test that dependent inventory updates exhibit good behavior on cancel - of the source project update - """ - task = RunProjectUpdate() - proj_update = ProjectUpdate.objects.create(project=project) - - kwargs = dict(source_project=project, source='scm', source_path='inventory_file', update_on_project_update=True, inventory=inventory) - - is1 = InventorySource.objects.create(name="test-scm-inv", **kwargs) - is2 = InventorySource.objects.create(name="test-scm-inv2", **kwargs) - - def user_cancels_project(pk): - ProjectUpdate.objects.all().update(cancel_flag=True) - - with mock.patch.object(RunInventoryUpdate, 'run') as iu_run_mock: - with mock.patch('awx.main.tasks.jobs.create_partition'): - iu_run_mock.side_effect = user_cancels_project - task._update_dependent_inventories(proj_update, [is1, is2]) - # Verify that it bails after 1st update, detecting a cancel - assert is2.inventory_updates.count() == 0 - iu_run_mock.assert_called_once() - - @pytest.fixture def mock_job_folder(request): pdd_path = tempfile.mkdtemp(prefix='awx_123_') diff --git a/awx/main/tests/unit/api/test_views.py b/awx/main/tests/unit/api/test_views.py index 8e7b7a73dc..7f69c816e7 100644 --- a/awx/main/tests/unit/api/test_views.py +++ b/awx/main/tests/unit/api/test_views.py @@ -69,21 +69,21 @@ class TestJobTemplateLabelList: class TestInventoryInventorySourcesUpdate: @pytest.mark.parametrize( - "can_update, can_access, is_source, is_up_on_proj, expected", + "can_update, can_access, is_source, expected", [ - (True, True, "ec2", False, [{'status': 'started', 'inventory_update': 1, 'inventory_source': 1}]), - (False, True, "gce", False, [{'status': 'Could not start because `can_update` returned False', 'inventory_source': 1}]), - (True, False, "scm", True, [{'status': 'started', 'inventory_update': 1, 'inventory_source': 1}]), + (True, True, "ec2", [{'status': 'started', 'inventory_update': 1, 'inventory_source': 1}]), + (False, True, "gce", [{'status': 'Could not start because `can_update` returned False', 'inventory_source': 1}]), + (True, False, "scm", [{'status': 'started', 'inventory_update': 1, 'inventory_source': 1}]), ], ) - def test_post(self, mocker, can_update, can_access, is_source, is_up_on_proj, expected): + def test_post(self, mocker, can_update, can_access, is_source, expected): class InventoryUpdate: id = 1 class Project: name = 'project' - InventorySource = namedtuple('InventorySource', ['source', 'update_on_project_update', 'pk', 'can_update', 'update', 'source_project']) + InventorySource = namedtuple('InventorySource', ['source', 'pk', 'can_update', 'update', 'source_project']) class InventorySources(object): def all(self): @@ -92,7 +92,6 @@ class TestInventoryInventorySourcesUpdate: pk=1, source=is_source, source_project=Project, - update_on_project_update=is_up_on_proj, can_update=can_update, update=lambda: InventoryUpdate, ) diff --git a/awx/main/tests/unit/models/test_inventory.py b/awx/main/tests/unit/models/test_inventory.py index 34a8000665..92d041b8e3 100644 --- a/awx/main/tests/unit/models/test_inventory.py +++ b/awx/main/tests/unit/models/test_inventory.py @@ -1,28 +1,13 @@ import pytest -from unittest import mock from django.core.exceptions import ValidationError from awx.main.models import ( - UnifiedJob, InventoryUpdate, InventorySource, ) -def test_cancel(mocker): - with mock.patch.object(UnifiedJob, 'cancel', return_value=True) as parent_cancel: - iu = InventoryUpdate() - - iu.save = mocker.MagicMock() - build_job_explanation_mock = mocker.MagicMock() - iu._build_job_explanation = mocker.MagicMock(return_value=build_job_explanation_mock) - - iu.cancel() - - parent_cancel.assert_called_with(is_chain=False, job_explanation=None) - - def test__build_job_explanation(): iu = InventoryUpdate(id=3, name='I_am_an_Inventory_Update') @@ -53,9 +38,3 @@ class TestControlledBySCM: with pytest.raises(ValidationError): inv_src.clean_source_path() - - def test_clean_update_on_launch_update_on_project_update(self): - inv_src = InventorySource(update_on_project_update=True, update_on_launch=True, source='scm') - - with pytest.raises(ValidationError): - inv_src.clean_update_on_launch() diff --git a/awx_collection/plugins/modules/inventory_source.py b/awx_collection/plugins/modules/inventory_source.py index 1072ff1314..f3000ee9ec 100644 --- a/awx_collection/plugins/modules/inventory_source.py +++ b/awx_collection/plugins/modules/inventory_source.py @@ -105,9 +105,6 @@ options: description: - Project to use as source with scm option type: str - update_on_project_update: - description: Update this source when the related project updates if source is C(scm) - type: bool state: description: - Desired state of the resource. @@ -181,7 +178,6 @@ def main(): update_on_launch=dict(type='bool'), update_cache_timeout=dict(type='int'), source_project=dict(), - update_on_project_update=dict(type='bool'), notification_templates_started=dict(type="list", elements='str'), notification_templates_success=dict(type="list", elements='str'), notification_templates_error=dict(type="list", elements='str'), @@ -273,7 +269,6 @@ def main(): 'verbosity', 'update_on_launch', 'update_cache_timeout', - 'update_on_project_update', 'enabled_var', 'enabled_value', 'host_filter', diff --git a/docs/inventory/scm_inventory.md b/docs/inventory/scm_inventory.md index bb48e01a80..f3342a1aa5 100644 --- a/docs/inventory/scm_inventory.md +++ b/docs/inventory/scm_inventory.md @@ -17,21 +17,6 @@ Additionally: - `source_vars` - if these are set on a "file" type inventory source then they will be passed to the environment vars when running - - `update_on_project_update` - if set, a project update of the source - project will automatically update this inventory source as a side effect - -If `update_on_project_update` is not set, then they can manually update -just the inventory source with a POST to its update endpoint, -`/inventory_sources/N/update/`. - -If `update_on_project_update` is set, the POST to the inventory source's -update endpoint will trigger an update of the source project, which may, -in turn, trigger an update of the inventory source. -Also, with this flag set, an update _of the project_ is -scheduled immediately after creation of the inventory source. -Also, if this flag is set, no inventory updates will be triggered -_unless the SCM revision of the project changes_. - ### RBAC @@ -52,17 +37,6 @@ This listing should be refreshed to the latest SCM info on a project update. If no inventory sources use a project as an SCM inventory source, then the inventory listing may not be refreshed on update. - -### Inventory Source Restriction - -Since automatic inventory updates (triggered by a project update) do not -go through the task system, typical protection against conflicting updates -is not available. To avoid problems, only one inventory source is allowed for -inventories that use this feature. That means that if an inventory source -has `source=scm` and `update_on_project_update=true`, it can be the only -inventory source for its inventory. - - ## Supported File Syntax > Any Inventory Ansible supports should be supported by this feature.