mirror of
https://github.com/ansible/awx.git
synced 2026-10-30 22:16:19 -02:30
Fix 500 on /api/v2/schedules/ from orphaned token cleanup schedule (#16660)
* Fix 500 error on /api/v2/schedules/ from orphaned token cleanup schedule delete_clear_tokens_sjt() relied on cascade to remove the associated schedule when deleting the cleanup_tokens SystemJobTemplate. In migration context with polymorphic models, the cascade did not reliably fire, leaving an orphaned schedule whose unified_job_template referenced a missing row. Explicitly delete associated schedules and clear stale next_schedule references before deleting the SJT. Add defensive handling in ScheduleSerializer.get_related() and get_summary_fields() so orphaned schedules degrade gracefully instead of raising RelatedObjectDoesNotExist. Fix the existing test assertion that used a JOIN-based query incapable of detecting orphans, and add coverage for next_schedule reference cleanup. Fixes: AAP-92564 * Add migration to clean up orphaned token cleanup schedules Databases that already ran migration 0204 may have orphaned "Cleanup Expired OAuth 2 Tokens" schedules left behind by the broken cascade. Remove them and clear any stale UnifiedJobTemplate.next_schedule references. Add a test that simulates the orphan scenario by deleting the UJT via raw SQL and verifying the migration repairs it. Ref: AAP-92564 * Re-parent migration 0212 for gateway backport to stable-2.7 Apply the gateway backport migration pattern (AAP-87599): re-parent 0212_cleanup_orphaned_token_schedules to depend on 0207_merge (the common ancestor with tower/stable-2.7) instead of 0211. Add 0213_merge_0211_0212 to converge the parallel paths on devel. This allows the backport to tower/stable-2.7 to use the identical 0212 migration file with no changes. Ref: AAP-92564 * Test next_schedule cleanup on a surviving template Use the cleanup_sessions SystemJobTemplate (which survives the deletion) as the next_schedule reference holder, so the assertion actually proves the UPDATE ... SET next_schedule = NULL logic works rather than being masked by the referenced UJT itself being deleted. Ref: AAP-92564 * Fix test: create surviving SJT instead of relying on migration data The CI test DB does not have the cleanup_sessions SystemJobTemplate from migration 0078. Use get_or_create to ensure the surviving UJT exists within the test itself. Ref: AAP-92564 * Renumber cleanup migration to 0208 and bump 0208-0211 to 0209-0212 Replace the gateway backport pattern (re-parented migration + merge migration) with a simpler approach: slot the cleanup migration as 0208 directly after the merge point, and renumber the subsequent migrations. This keeps the migration graph linear on all branches and preserves the ability to backport 0209-0212 individually in the future. Ref: AAP-92564 --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
4c290176ef
commit
dad4da74b9
+19
-10
@@ -5792,24 +5792,33 @@ class ScheduleSerializer(PromptFieldCleanTextMixin, LaunchConfigurationBaseSeria
|
||||
def get_related(self, obj):
|
||||
res = super(ScheduleSerializer, self).get_related(obj)
|
||||
res.update(dict(unified_jobs=self.reverse('api:schedule_unified_jobs_list', kwargs={'pk': obj.pk})))
|
||||
if obj.unified_job_template:
|
||||
res['unified_job_template'] = obj.unified_job_template.get_absolute_url(self.context.get('request'))
|
||||
try:
|
||||
ujt = obj.unified_job_template
|
||||
except ObjectDoesNotExist:
|
||||
ujt = None
|
||||
if ujt:
|
||||
res['unified_job_template'] = ujt.get_absolute_url(self.context.get('request'))
|
||||
try:
|
||||
if obj.unified_job_template.project:
|
||||
res['project'] = obj.unified_job_template.project.get_absolute_url(self.context.get('request'))
|
||||
if ujt.project:
|
||||
res['project'] = ujt.project.get_absolute_url(self.context.get('request'))
|
||||
except ObjectDoesNotExist:
|
||||
pass
|
||||
if obj.inventory:
|
||||
res['inventory'] = obj.inventory.get_absolute_url(self.context.get('request'))
|
||||
elif obj.unified_job_template and getattr(obj.unified_job_template, 'inventory', None):
|
||||
res['inventory'] = obj.unified_job_template.inventory.get_absolute_url(self.context.get('request'))
|
||||
elif ujt and getattr(ujt, 'inventory', None):
|
||||
res['inventory'] = ujt.inventory.get_absolute_url(self.context.get('request'))
|
||||
return res
|
||||
|
||||
def get_summary_fields(self, obj):
|
||||
summary_fields = super(ScheduleSerializer, self).get_summary_fields(obj)
|
||||
|
||||
if isinstance(obj.unified_job_template, SystemJobTemplate):
|
||||
summary_fields['unified_job_template']['job_type'] = obj.unified_job_template.job_type
|
||||
try:
|
||||
ujt = obj.unified_job_template
|
||||
except ObjectDoesNotExist:
|
||||
return summary_fields
|
||||
|
||||
if isinstance(ujt, SystemJobTemplate):
|
||||
summary_fields['unified_job_template']['job_type'] = ujt.job_type
|
||||
|
||||
# We are not showing instance groups on summary fields because JTs don't either
|
||||
|
||||
@@ -5817,8 +5826,8 @@ class ScheduleSerializer(PromptFieldCleanTextMixin, LaunchConfigurationBaseSeria
|
||||
return summary_fields
|
||||
|
||||
inventory = None
|
||||
if obj.unified_job_template and getattr(obj.unified_job_template, 'inventory', None):
|
||||
inventory = obj.unified_job_template.inventory
|
||||
if ujt and getattr(ujt, 'inventory', None):
|
||||
inventory = ujt.inventory
|
||||
else:
|
||||
return summary_fields
|
||||
|
||||
|
||||
Reference in New Issue
Block a user