Merge pull request #12366 from fosterseth/remove_update_on_project_update

Remove deprecated field update_on_project_update
This commit is contained in:
Seth Foster
2022-06-28 13:15:57 -04:00
committed by GitHub
17 changed files with 64 additions and 367 deletions

View File

@@ -2073,7 +2073,7 @@ class InventorySourceSerializer(UnifiedJobTemplateSerializer, InventorySourceOpt
class Meta: class Meta:
model = InventorySource 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_update_failed',
'last_updated', 'last_updated',
) # Backwards compatibility. ) # Backwards compatibility.
@@ -2136,11 +2136,6 @@ class InventorySourceSerializer(UnifiedJobTemplateSerializer, InventorySourceOpt
raise serializers.ValidationError(_("Cannot use manual project for SCM-based inventory.")) raise serializers.ValidationError(_("Cannot use manual project for SCM-based inventory."))
return value 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): def validate_inventory(self, value):
if value and value.kind == 'smart': if value and value.kind == 'smart':
raise serializers.ValidationError({"detail": _("Cannot create Inventory Source for Smart Inventory")}) 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: 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.")}) raise serializers.ValidationError({"source_project": _("Project required for scm type sources.")})
else: 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: if redundant_scm_fields:
raise serializers.ValidationError({"detail": _("Cannot set %s if not SCM type." % ' '.join(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.')) raise serializers.ValidationError(_('Inventory Source must be a cloud resource.'))
elif type(value) == Project and value.scm_type == '': elif type(value) == Project and value.scm_type == '':
raise serializers.ValidationError(_('Manual Project cannot have a schedule set.')) 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 return value
def validate(self, attrs): def validate(self, attrs):

View File

@@ -115,7 +115,6 @@ from awx.api.metadata import RoleMetadata
from awx.main.constants import ACTIVE_STATES, SURVEY_TYPE_MAPPING from awx.main.constants import ACTIVE_STATES, SURVEY_TYPE_MAPPING
from awx.main.scheduler.dag_workflow import WorkflowDAG from awx.main.scheduler.dag_workflow import WorkflowDAG
from awx.api.views.mixin import ( from awx.api.views.mixin import (
ControlledByScmMixin,
InstanceGroupMembershipMixin, InstanceGroupMembershipMixin,
OrganizationCountsMixin, OrganizationCountsMixin,
RelatedJobsPreventDeleteMixin, RelatedJobsPreventDeleteMixin,
@@ -1675,7 +1674,7 @@ class HostList(HostRelatedSearchMixin, ListCreateAPIView):
return Response(dict(error=_(str(e))), status=status.HTTP_400_BAD_REQUEST) 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 always_allow_superuser = False
model = models.Host model = models.Host
@@ -1709,7 +1708,7 @@ class InventoryHostsList(HostRelatedSearchMixin, SubListCreateAttachDetachAPIVie
return qs return qs
class HostGroupsList(ControlledByScmMixin, SubListCreateAttachDetachAPIView): class HostGroupsList(SubListCreateAttachDetachAPIView):
'''the list of groups a host is directly a member of''' '''the list of groups a host is directly a member of'''
model = models.Group model = models.Group
@@ -1825,7 +1824,7 @@ class EnforceParentRelationshipMixin(object):
return super(EnforceParentRelationshipMixin, self).create(request, *args, **kwargs) return super(EnforceParentRelationshipMixin, self).create(request, *args, **kwargs)
class GroupChildrenList(ControlledByScmMixin, EnforceParentRelationshipMixin, SubListCreateAttachDetachAPIView): class GroupChildrenList(EnforceParentRelationshipMixin, SubListCreateAttachDetachAPIView):
model = models.Group model = models.Group
serializer_class = serializers.GroupSerializer serializer_class = serializers.GroupSerializer
@@ -1871,7 +1870,7 @@ class GroupPotentialChildrenList(SubListAPIView):
return qs.exclude(pk__in=except_pks) return qs.exclude(pk__in=except_pks)
class GroupHostsList(HostRelatedSearchMixin, ControlledByScmMixin, SubListCreateAttachDetachAPIView): class GroupHostsList(HostRelatedSearchMixin, SubListCreateAttachDetachAPIView):
'''the list of hosts directly below a group''' '''the list of hosts directly below a group'''
model = models.Host model = models.Host
@@ -1935,7 +1934,7 @@ class GroupActivityStreamList(SubListAPIView):
return qs.filter(Q(group=parent) | Q(host__in=parent.hosts.all())) return qs.filter(Q(group=parent) | Q(host__in=parent.hosts.all()))
class GroupDetail(RelatedJobsPreventDeleteMixin, ControlledByScmMixin, RetrieveUpdateDestroyAPIView): class GroupDetail(RelatedJobsPreventDeleteMixin, RetrieveUpdateDestroyAPIView):
model = models.Group model = models.Group
serializer_class = serializers.GroupSerializer serializer_class = serializers.GroupSerializer

View File

@@ -41,7 +41,7 @@ from awx.api.serializers import (
JobTemplateSerializer, JobTemplateSerializer,
LabelSerializer, LabelSerializer,
) )
from awx.api.views.mixin import RelatedJobsPreventDeleteMixin, ControlledByScmMixin from awx.api.views.mixin import RelatedJobsPreventDeleteMixin
from awx.api.pagination import UnifiedJobEventPagination from awx.api.pagination import UnifiedJobEventPagination
@@ -75,7 +75,7 @@ class InventoryList(ListCreateAPIView):
serializer_class = InventorySerializer serializer_class = InventorySerializer
class InventoryDetail(RelatedJobsPreventDeleteMixin, ControlledByScmMixin, RetrieveUpdateDestroyAPIView): class InventoryDetail(RelatedJobsPreventDeleteMixin, RetrieveUpdateDestroyAPIView):
model = Inventory model = Inventory
serializer_class = InventorySerializer serializer_class = InventorySerializer

View File

@@ -10,13 +10,12 @@ from django.shortcuts import get_object_or_404
from django.utils.timezone import now from django.utils.timezone import now
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from rest_framework.permissions import SAFE_METHODS
from rest_framework.exceptions import PermissionDenied from rest_framework.exceptions import PermissionDenied
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework import status from rest_framework import status
from awx.main.constants import ACTIVE_STATES 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.ha import Instance, InstanceGroup
from awx.main.models.organization import Team from awx.main.models.organization import Team
from awx.main.models.projects import Project from awx.main.models.projects import Project
@@ -186,35 +185,6 @@ class OrganizationCountsMixin(object):
return full_context 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): class NoTruncateMixin(object):
def get_serializer_context(self): def get_serializer_context(self):
context = super().get_serializer_context() context = super().get_serializer_context()

View File

@@ -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',
),
]

View File

@@ -985,22 +985,11 @@ class InventorySource(UnifiedJobTemplate, InventorySourceOptions, CustomVirtualE
default=None, default=None,
null=True, 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( update_on_launch = models.BooleanField(
default=False, default=False,
) )
update_cache_timeout = models.PositiveIntegerField( update_cache_timeout = models.PositiveIntegerField(
default=0, default=0,
) )
@@ -1038,14 +1027,6 @@ class InventorySource(UnifiedJobTemplate, InventorySourceOptions, CustomVirtualE
self.name = 'inventory source (%s)' % replace_text self.name = 'inventory source (%s)' % replace_text
if 'name' not in update_fields: if 'name' not in update_fields:
update_fields.append('name') 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. # Do the actual save.
super(InventorySource, self).save(*args, **kwargs) super(InventorySource, self).save(*args, **kwargs)
@@ -1054,10 +1035,6 @@ class InventorySource(UnifiedJobTemplate, InventorySourceOptions, CustomVirtualE
if replace_text in self.name: if replace_text in self.name:
self.name = self.name.replace(replace_text, str(self.pk)) self.name = self.name.replace(replace_text, str(self.pk))
super(InventorySource, self).save(update_fields=['name']) 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 not getattr(_inventory_updates, 'is_updating', False):
if self.inventory is not None: if self.inventory is not None:
self.inventory.update_computed_fields() 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)) 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): def clean_source_path(self):
if self.source != 'scm' and self.source_path: if self.source != 'scm' and self.source_path:
raise ValidationError(_("Cannot set source_path if not SCM type.")) 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 self.global_instance_groups
return selected_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 CustomInventoryScript(CommonModelNameNotUnique, ResourceMixin):
class Meta: class Meta:

View File

@@ -533,7 +533,7 @@ class UnifiedJob(
('workflow', _('Workflow')), # Job was started from a workflow job. ('workflow', _('Workflow')), # Job was started from a workflow job.
('webhook', _('Webhook')), # Job was started from a webhook event. ('webhook', _('Webhook')), # Job was started from a webhook event.
('sync', _('Sync')), # Job was started from a project sync. ('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',) PASSWORD_FIELDS = ('start_args',)

View File

@@ -19,7 +19,6 @@ from uuid import uuid4
# Django # Django
from django.conf import settings from django.conf import settings
from django.db import transaction
# Runner # Runner
@@ -34,7 +33,6 @@ from gitdb.exc import BadName as BadGitName
from awx.main.dispatch.publish import task from awx.main.dispatch.publish import task
from awx.main.dispatch import get_local_queuename from awx.main.dispatch import get_local_queuename
from awx.main.constants import ( from awx.main.constants import (
ACTIVE_STATES,
PRIVILEGE_ESCALATION_METHODS, PRIVILEGE_ESCALATION_METHODS,
STANDARD_INVENTORY_UPDATE_ENV, STANDARD_INVENTORY_UPDATE_ENV,
JOB_FOLDER_PREFIX, JOB_FOLDER_PREFIX,
@@ -1168,64 +1166,6 @@ class RunProjectUpdate(BaseTask):
d[r'^Are you sure you want to continue connecting \(yes/no\)\?\s*?$'] = 'yes' d[r'^Are you sure you want to continue connecting \(yes/no\)\?\s*?$'] = 'yes'
return d 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): def release_lock(self, instance):
try: try:
fcntl.lockf(self.lock_fd, fcntl.LOCK_UN) fcntl.lockf(self.lock_fd, fcntl.LOCK_UN)
@@ -1435,12 +1375,6 @@ class RunProjectUpdate(BaseTask):
p.inventory_files = p.inventories p.inventory_files = p.inventories
p.save(update_fields=['scm_revision', 'playbook_files', 'inventory_files']) 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): def build_execution_environment_params(self, instance, private_data_dir):
if settings.IS_K8S: if settings.IS_K8S:
return {} return {}
@@ -1620,9 +1554,7 @@ class RunInventoryUpdate(BaseTask):
source_project = None source_project = None
if inventory_update.inventory_source: if inventory_update.inventory_source:
source_project = inventory_update.inventory_source.source_project source_project = inventory_update.inventory_source.source_project
if ( if inventory_update.source == 'scm' and source_project and source_project.scm_type: # never ever update manual projects
inventory_update.source == 'scm' and inventory_update.launch_type != '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 # Check if the content cache exists, so that we do not unnecessarily re-download roles
sync_needs = ['update_{}'.format(source_project.scm_type)] 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 = project_update_task(job_private_data_dir=private_data_dir)
sync_task.run(local_project_sync.id) sync_task.run(local_project_sync.id)
local_project_sync.refresh_from_db() 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: except Exception:
inventory_update = self.update_model( inventory_update = self.update_model(
inventory_update.pk, inventory_update.pk,
@@ -1667,9 +1597,6 @@ class RunInventoryUpdate(BaseTask):
), ),
) )
raise 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): def post_run_hook(self, inventory_update, status):
super(RunInventoryUpdate, self).post_run_hook(inventory_update, status) super(RunInventoryUpdate, self).post_run_hook(inventory_update, status)

View File

@@ -9,9 +9,7 @@ from awx.api.versioning import reverse
@pytest.fixture @pytest.fixture
def ec2_source(inventory, project): def ec2_source(inventory, project):
with mock.patch('awx.main.models.unified_jobs.UnifiedJobTemplate.update'): with mock.patch('awx.main.models.unified_jobs.UnifiedJobTemplate.update'):
return inventory.inventory_sources.create( return inventory.inventory_sources.create(name='some_source', source='ec2', source_project=project)
name='some_source', update_on_project_update=True, source='ec2', source_project=project, scm_last_revision=project.scm_revision
)
@pytest.fixture @pytest.fixture

View File

@@ -13,9 +13,7 @@ from awx.main.models import InventorySource, Inventory, ActivityStream
@pytest.fixture @pytest.fixture
def scm_inventory(inventory, project): def scm_inventory(inventory, project):
with mock.patch('awx.main.models.unified_jobs.UnifiedJobTemplate.update'): with mock.patch('awx.main.models.unified_jobs.UnifiedJobTemplate.update'):
inventory.inventory_sources.create( inventory.inventory_sources.create(name='foobar', source='scm', source_project=project)
name='foobar', update_on_project_update=True, source='scm', source_project=project, scm_last_revision=project.scm_revision
)
return inventory return inventory
@@ -23,9 +21,7 @@ def scm_inventory(inventory, project):
def factory_scm_inventory(inventory, project): def factory_scm_inventory(inventory, project):
def fn(**kwargs): def fn(**kwargs):
with mock.patch('awx.main.models.unified_jobs.UnifiedJobTemplate.update'): with mock.patch('awx.main.models.unified_jobs.UnifiedJobTemplate.update'):
return inventory.inventory_sources.create( return inventory.inventory_sources.create(source_project=project, overwrite_vars=True, source='scm', **kwargs)
source_project=project, overwrite_vars=True, source='scm', scm_last_revision=project.scm_revision, **kwargs
)
return fn return fn
@@ -544,15 +540,12 @@ class TestControlledBySCM:
def test_safe_method_works(self, get, options, scm_inventory, admin_user): def test_safe_method_works(self, get, options, scm_inventory, admin_user):
get(scm_inventory.get_absolute_url(), admin_user, expect=200) get(scm_inventory.get_absolute_url(), admin_user, expect=200)
options(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): def test_vars_edit_reset(self, patch, scm_inventory, admin_user):
patch(scm_inventory.get_absolute_url(), {'variables': 'hello: world'}, admin_user, expect=200) 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): def test_name_edit_allowed(self, patch, scm_inventory, admin_user):
patch(scm_inventory.get_absolute_url(), {'variables': '---', 'name': 'newname'}, admin_user, expect=200) 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): def test_host_associations_reset(self, post, scm_inventory, admin_user):
inv_src = scm_inventory.inventory_sources.first() inv_src = scm_inventory.inventory_sources.first()
@@ -560,14 +553,12 @@ class TestControlledBySCM:
g = inv_src.groups.create(name='fooland', inventory=scm_inventory) 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: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) 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): def test_group_group_associations_reset(self, post, scm_inventory, admin_user):
inv_src = scm_inventory.inventory_sources.first() inv_src = scm_inventory.inventory_sources.first()
g1 = inv_src.groups.create(name='barland', inventory=scm_inventory) g1 = inv_src.groups.create(name='barland', inventory=scm_inventory)
g2 = inv_src.groups.create(name='fooland', 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) 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): def test_host_group_delete_reset(self, delete, scm_inventory, admin_user):
inv_src = scm_inventory.inventory_sources.first() inv_src = scm_inventory.inventory_sources.first()
@@ -575,7 +566,6 @@ class TestControlledBySCM:
g = inv_src.groups.create(name='fooland', inventory=scm_inventory) g = inv_src.groups.create(name='fooland', inventory=scm_inventory)
delete(h.get_absolute_url(), admin_user, expect=204) delete(h.get_absolute_url(), admin_user, expect=204)
delete(g.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): def test_remove_scm_inv_src(self, delete, scm_inventory, admin_user):
inv_src = scm_inventory.inventory_sources.first() inv_src = scm_inventory.inventory_sources.first()
@@ -588,7 +578,6 @@ class TestControlledBySCM:
{ {
'name': 'new inv src', 'name': 'new inv src',
'source_project': project.pk, 'source_project': project.pk,
'update_on_project_update': False,
'source': 'scm', 'source': 'scm',
'overwrite_vars': True, 'overwrite_vars': True,
'source_vars': 'plugin: a.b.c', 'source_vars': 'plugin: a.b.c',
@@ -597,27 +586,6 @@ class TestControlledBySCM:
expect=201, 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): def test_adding_inv_src_without_proj_access_prohibited(self, post, project, inventory, rando):
inventory.admin_role.members.add(rando) inventory.admin_role.members.add(rando)
post( post(

View File

@@ -347,9 +347,7 @@ def scm_inventory_source(inventory, project):
source_project=project, source_project=project,
source='scm', source='scm',
source_path='inventory_file', source_path='inventory_file',
update_on_project_update=True,
inventory=inventory, inventory=inventory,
scm_last_revision=project.scm_revision,
) )
with mock.patch('awx.main.models.unified_jobs.UnifiedJobTemplate.update'): with mock.patch('awx.main.models.unified_jobs.UnifiedJobTemplate.update'):
inv_src.save() inv_src.save()

View File

@@ -3,8 +3,6 @@
import pytest import pytest
from unittest import mock from unittest import mock
from django.core.exceptions import ValidationError
# AWX # AWX
from awx.main.models import Host, Inventory, InventorySource, InventoryUpdate, CredentialType, Credential, Job from awx.main.models import Host, Inventory, InventorySource, InventoryUpdate, CredentialType, Credential, Job
from awx.main.constants import CLOUD_PROVIDERS from awx.main.constants import CLOUD_PROVIDERS
@@ -123,19 +121,6 @@ class TestActiveCount:
@pytest.mark.django_db @pytest.mark.django_db
class TestSCMUpdateFeatures: 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): def test_source_location(self, scm_inventory_source):
# Combines project directory with the inventory file specified # Combines project directory with the inventory file specified
inventory_update = InventoryUpdate(inventory_source=scm_inventory_source, source_path=scm_inventory_source.source_path) 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()] 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 @pytest.mark.django_db
class TestInventorySourceInjectors: class TestInventorySourceInjectors:
def test_extra_credentials(self, project, credential): def test_extra_credentials(self, project, credential):

View File

@@ -4,9 +4,8 @@ import os
import tempfile import tempfile
import shutil 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.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 @pytest.fixture
@@ -27,63 +26,6 @@ def test_no_worker_info_on_AWX_nodes(node_type):
execution_node_health_check(hostname) 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 @pytest.fixture
def mock_job_folder(request): def mock_job_folder(request):
pdd_path = tempfile.mkdtemp(prefix='awx_123_') pdd_path = tempfile.mkdtemp(prefix='awx_123_')

View File

@@ -69,21 +69,21 @@ class TestJobTemplateLabelList:
class TestInventoryInventorySourcesUpdate: class TestInventoryInventorySourcesUpdate:
@pytest.mark.parametrize( @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}]), (True, True, "ec2", [{'status': 'started', 'inventory_update': 1, 'inventory_source': 1}]),
(False, True, "gce", False, [{'status': 'Could not start because `can_update` returned False', 'inventory_source': 1}]), (False, True, "gce", [{'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, 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: class InventoryUpdate:
id = 1 id = 1
class Project: class Project:
name = '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): class InventorySources(object):
def all(self): def all(self):
@@ -92,7 +92,6 @@ class TestInventoryInventorySourcesUpdate:
pk=1, pk=1,
source=is_source, source=is_source,
source_project=Project, source_project=Project,
update_on_project_update=is_up_on_proj,
can_update=can_update, can_update=can_update,
update=lambda: InventoryUpdate, update=lambda: InventoryUpdate,
) )

View File

@@ -1,28 +1,13 @@
import pytest import pytest
from unittest import mock
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from awx.main.models import ( from awx.main.models import (
UnifiedJob,
InventoryUpdate, InventoryUpdate,
InventorySource, 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(): def test__build_job_explanation():
iu = InventoryUpdate(id=3, name='I_am_an_Inventory_Update') iu = InventoryUpdate(id=3, name='I_am_an_Inventory_Update')
@@ -53,9 +38,3 @@ class TestControlledBySCM:
with pytest.raises(ValidationError): with pytest.raises(ValidationError):
inv_src.clean_source_path() 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()

View File

@@ -105,9 +105,6 @@ options:
description: description:
- Project to use as source with scm option - Project to use as source with scm option
type: str type: str
update_on_project_update:
description: Update this source when the related project updates if source is C(scm)
type: bool
state: state:
description: description:
- Desired state of the resource. - Desired state of the resource.
@@ -181,7 +178,6 @@ def main():
update_on_launch=dict(type='bool'), update_on_launch=dict(type='bool'),
update_cache_timeout=dict(type='int'), update_cache_timeout=dict(type='int'),
source_project=dict(), source_project=dict(),
update_on_project_update=dict(type='bool'),
notification_templates_started=dict(type="list", elements='str'), notification_templates_started=dict(type="list", elements='str'),
notification_templates_success=dict(type="list", elements='str'), notification_templates_success=dict(type="list", elements='str'),
notification_templates_error=dict(type="list", elements='str'), notification_templates_error=dict(type="list", elements='str'),
@@ -273,7 +269,6 @@ def main():
'verbosity', 'verbosity',
'update_on_launch', 'update_on_launch',
'update_cache_timeout', 'update_cache_timeout',
'update_on_project_update',
'enabled_var', 'enabled_var',
'enabled_value', 'enabled_value',
'host_filter', 'host_filter',

View File

@@ -17,21 +17,6 @@ Additionally:
- `source_vars` - if these are set on a "file" type inventory source - `source_vars` - if these are set on a "file" type inventory source
then they will be passed to the environment vars when running 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 ### 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 If no inventory sources use a project as an SCM inventory source, then
the inventory listing may not be refreshed on update. 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 ## Supported File Syntax
> Any Inventory Ansible supports should be supported by this feature. > Any Inventory Ansible supports should be supported by this feature.