diff --git a/awx/api/serializers.py b/awx/api/serializers.py index 996fb627f7..9297a6f117 100644 --- a/awx/api/serializers.py +++ b/awx/api/serializers.py @@ -1238,13 +1238,41 @@ class HostSerializer(BaseSerializerWithVariables): class GroupSerializer(BaseSerializerWithVariables): - show_capabilities = ['copy', 'edit', 'delete'] + inventory_source = serializers.SerializerMethodField( + help_text=_('Dedicated inventory source for the group, will be removed in 3.3.')) class Meta: model = Group fields = ('*', 'inventory', 'variables', 'has_active_failures', 'total_hosts', 'hosts_with_active_failures', 'total_groups', - 'groups_with_active_failures', 'has_inventory_sources') + 'groups_with_active_failures', 'has_inventory_sources', 'inventory_source') + + def get_fields(self): # TODO: remove in 3.3 + fields = super(GroupSerializer, self).get_fields() + if not self.V1: + fields.pop('inventory_source') + return fields + + @property + def V1(self): + request = self.context.get('request') + # TODO: use the better version-getter after merged with other branches + if request and request.version == 'v1': + return True + return False + + @property + def show_capabilities(self): # TODO: consolidate in 3.3 + if self.V1: + return ['copy', 'edit', 'start', 'schedule', 'delete'] + else: + return ['copy', 'edit', 'delete'] + + def get_inventory_source(self, obj): # TODO: remove in 3.3 + try: + return obj.deprecated_inventory_source.id + except Group.deprecated_inventory_source.RelatedObjectDoesNotExist: + return None def build_relational_field(self, field_name, relation_info): field_class, field_kwargs = super(GroupSerializer, self).build_relational_field(field_name, relation_info) @@ -1268,10 +1296,22 @@ class GroupSerializer(BaseSerializerWithVariables): inventory_sources = self.reverse('api:group_inventory_sources_list', kwargs={'pk': obj.pk}), ad_hoc_commands = self.reverse('api:group_ad_hoc_commands_list', kwargs={'pk': obj.pk}), )) + if self.V1: # TODO: remove in 3.3 + try: + res['inventory_source'] = self.reverse('api:inventory_source_detail', + kwargs={'pk': obj.deprecated_inventory_source.pk}) + except Group.deprecated_inventory_source.RelatedObjectDoesNotExist: + res['inventory_source'] = None if obj.inventory: res['inventory'] = self.reverse('api:inventory_detail', kwargs={'pk': obj.inventory.pk}) return res + def create(self, validated_data): # TODO: remove in 3.3 + instance = super(GroupSerializer, self).create(validated_data) + if self.V1: + InventorySource.objects.create(deprecated_group=instance, inventory=instance.inventory) + return instance + def validate_name(self, value): if value in ('all', '_meta'): raise serializers.ValidationError(_('Invalid group name.')) @@ -1428,11 +1468,13 @@ class InventorySourceSerializer(UnifiedJobTemplateSerializer, InventorySourceOpt last_update_failed = serializers.BooleanField(read_only=True) last_updated = serializers.DateTimeField(read_only=True) show_capabilities = ['start', 'schedule', 'edit', 'delete'] + group = serializers.SerializerMethodField( + help_text=_('Automatic group relationship, will be removed in 3.3')) class Meta: model = InventorySource fields = ('*', 'name', 'inventory', 'update_on_launch', 'update_cache_timeout') + \ - ('last_update_failed', 'last_updated') # Backwards compatibility. + ('last_update_failed', 'last_updated', 'group') # Backwards compatibility. def get_related(self, obj): res = super(InventorySourceSerializer, self).get_related(obj) @@ -1456,8 +1498,30 @@ class InventorySourceSerializer(UnifiedJobTemplateSerializer, InventorySourceOpt if obj.last_update: res['last_update'] = self.reverse('api:inventory_update_detail', kwargs={'pk': obj.last_update.pk}) + if self.V1: # TODO: remove in 3.3 + res['group'] = None + if obj.deprecated_group: + res['group'] = self.reverse('api:group_detail', kwargs={'pk': obj.deprecated_group.pk}) return res + def get_fields(self): # TODO: remove in 3.3 + fields = super(InventorySourceSerializer, self).get_fields() + if not self.V1: + fields.pop('group') + return fields + + def get_group(self, obj): # TODO: remove in 3.3 + if obj.deprecated_group: + return obj.deprecated_group.id + return None + + @property + def V1(self): # TODO: use the better version-getter after merged with other branches + request = self.context.get('request') + if request and request.version == 'v1': + return True + return False + def to_representation(self, obj): ret = super(InventorySourceSerializer, self).to_representation(obj) if obj is None: diff --git a/awx/api/views.py b/awx/api/views.py index 1984f4f136..f70017b7ce 100644 --- a/awx/api/views.py +++ b/awx/api/views.py @@ -1855,7 +1855,7 @@ class GroupList(ListCreateAPIView): model = Group serializer_class = GroupSerializer - capabilities_prefetch = ['inventory.admin', 'inventory.adhoc', 'inventory.update'] + capabilities_prefetch = ['inventory.admin', 'inventory.adhoc'] class EnforceParentRelationshipMixin(object): @@ -1999,6 +1999,11 @@ class GroupDetail(RetrieveUpdateDestroyAPIView): obj = self.get_object() if not request.user.can_access(self.model, 'delete', obj): raise PermissionDenied() + if self.request.version == 'v1': # TODO: deletion of automatic inventory_source, remove in 3.3 + try: + obj.deprecated_inventory_source.delete() + except Group.deprecated_inventory_source.RelatedObjectDoesNotExist: + pass obj.delete_recursive() return Response(status=status.HTTP_204_NO_CONTENT) @@ -2189,7 +2194,7 @@ class InventorySourceList(ListAPIView): new_in_14 = True -class InventorySourceDetail(RetrieveUpdateAPIView): +class InventorySourceDetail(RetrieveUpdateDestroyAPIView): model = InventorySource serializer_class = InventorySourceSerializer diff --git a/awx/main/access.py b/awx/main/access.py index a14ccc1ca1..cac7ac88f1 100644 --- a/awx/main/access.py +++ b/awx/main/access.py @@ -346,6 +346,17 @@ class BaseAccess(object): elif display_method == 'copy' and isinstance(obj, WorkflowJobTemplate) and obj.organization_id is None: user_capabilities[display_method] = self.user.is_superuser continue + elif display_method in ['start', 'schedule'] and isinstance(obj, Group): # TODO: remove in 3.3 + try: + if obj.deprecated_inventory_source and not obj.deprecated_inventory_source._can_update(): + user_capabilities[display_method] = False + continue + except Group.deprecated_inventory_source.RelatedObjectDoesNotExist: + user_capabilities[display_method] = False + continue + if obj.inventory_source and not obj.inventory_source._can_update(): + user_capabilities[display_method] = False + continue elif display_method in ['start', 'schedule'] and isinstance(obj, (Project)): if obj.scm_type == '': user_capabilities[display_method] = False @@ -720,6 +731,19 @@ class GroupAccess(BaseAccess): "active_jobs": active_jobs}) return True + def can_start(self, obj, validate_license=True): + # TODO: Delete for 3.3, only used by v1 serializer + # Used as another alias to inventory_source start access for user_capabilities + if obj: + try: + return self.user.can_access( + InventorySource, 'start', obj.deprecated_inventory_source, + validate_license=validate_license) + obj.deprecated_inventory_source + except Group.deprecated_inventory_source.RelatedObjectDoesNotExist: + return False + return False + class InventorySourceAccess(BaseAccess): ''' diff --git a/awx/main/migrations/0037_v320_release.py b/awx/main/migrations/0037_v320_release.py index a9745354bd..37a3a0795e 100644 --- a/awx/main/migrations/0037_v320_release.py +++ b/awx/main/migrations/0037_v320_release.py @@ -28,7 +28,7 @@ class Migration(migrations.Migration): migrations.AlterField( model_name='inventorysource', name='deprecated_group', - field=models.ForeignKey(related_name='deprecated_inventory_source', default=None, null=True, to='main.Group'), + field=models.OneToOneField(related_name='deprecated_inventory_source', null=True, default=None, to='main.Group'), ), migrations.AlterField( model_name='inventorysource', diff --git a/awx/main/migrations/0038_v320_data_migrations.py b/awx/main/migrations/0038_v320_data_migrations.py index 5b0f921cd6..37491c2dce 100644 --- a/awx/main/migrations/0038_v320_data_migrations.py +++ b/awx/main/migrations/0038_v320_data_migrations.py @@ -19,7 +19,6 @@ class Migration(migrations.Migration): operations = [ # Inventory Refresh migrations.RunPython(migration_utils.set_current_apps_for_migrations), - migrations.RunPython(invsrc.remove_manual_inventory_sources), migrations.RunPython(invsrc.remove_inventory_source_with_no_inventory_link), migrations.RunPython(invsrc.rename_inventory_sources), ] diff --git a/awx/main/migrations/_inventory_source.py b/awx/main/migrations/_inventory_source.py index a157bb2eb6..dd66bd84d5 100644 --- a/awx/main/migrations/_inventory_source.py +++ b/awx/main/migrations/_inventory_source.py @@ -10,6 +10,7 @@ def remove_manual_inventory_sources(apps, schema_editor): Group creation and we would use the parent Group as our interface for the user. During that process we would create InventorySource that had a source of "manual". ''' + # TODO: use this in the 3.3 data migrations InventorySource = apps.get_model('main', 'InventorySource') # see models/inventory.py SOURCE_CHOICES - ('', _('Manual')) logger.debug("Removing all Manual InventorySource from database.") diff --git a/awx/main/models/inventory.py b/awx/main/models/inventory.py index ecb702adc9..1d4792fa7a 100644 --- a/awx/main/models/inventory.py +++ b/awx/main/models/inventory.py @@ -1063,7 +1063,7 @@ class InventorySource(UnifiedJobTemplate, InventorySourceOptions): on_delete=models.CASCADE, ) - deprecated_group = models.ForeignKey( + deprecated_group = models.OneToOneField( 'Group', related_name='deprecated_inventory_source', null=True, @@ -1178,6 +1178,16 @@ class InventorySource(UnifiedJobTemplate, InventorySourceOptions): success=list(success_notification_templates), any=list(any_notification_templates)) + def clean_source(self): # TODO: remove in 3.3 + source = self.source + if source and self.deprecated_group: + qs = self.deprecated_group.inventory_sources.filter(source__in=CLOUD_INVENTORY_SOURCES) + existing_sources = qs.exclude(pk=self.pk) + if existing_sources.count(): + s = u', '.join([x.deprecated_group.name for x in existing_sources]) + raise ValidationError(_('Unable to configure this item for cloud sync. It is already managed by %s.') % s) + return source + class InventoryUpdate(UnifiedJob, InventorySourceOptions, JobNotificationMixin): ''' @@ -1212,6 +1222,8 @@ class InventoryUpdate(UnifiedJob, InventorySourceOptions, JobNotificationMixin): def websocket_emit_data(self): websocket_data = super(InventoryUpdate, self).websocket_emit_data() + if self.inventory_source.deprecated_group is not None: # TODO: remove in 3.3 + websocket_data.update(dict(group_id=self.inventory_source.deprecated_group.id)) return websocket_data def save(self, *args, **kwargs): diff --git a/awx/main/signals.py b/awx/main/signals.py index 5f82d8e26a..39c2822fed 100644 --- a/awx/main/signals.py +++ b/awx/main/signals.py @@ -372,6 +372,10 @@ model_serializer_mapping = { def activity_stream_create(sender, instance, created, **kwargs): if created and activity_stream_enabled: + # TODO: remove deprecated_group conditional in 3.3 + # Skip recording any inventory source directly associated with a group. + if isinstance(instance, InventorySource) and instance.deprecated_group: + return object1 = camelcase_to_underscore(instance.__class__.__name__) changes = model_to_dict(instance, model_serializer_mapping) # Special case where Job survey password variables need to be hidden @@ -417,6 +421,10 @@ def activity_stream_update(sender, instance, **kwargs): def activity_stream_delete(sender, instance, **kwargs): if not activity_stream_enabled: return + # TODO: remove deprecated_group conditional in 3.3 + # Skip recording any inventory source directly associated with a group. + if isinstance(instance, InventorySource) and instance.deprecated_group: + return changes = model_to_dict(instance) object1 = camelcase_to_underscore(instance.__class__.__name__) activity_entry = ActivityStream(