Upgrade to Django 5.2 LTS (#16185)

Upgrade to Django 5.2 LTS with compatibility fixes across fields, migrations, dispatch config, tests, and dev deps.

Dependencies:
- Upgrade django to 5.2.8 and relax requirements.in to >=5.2,<5.3.
- Bump django-debug-toolbar to >=6.0 for compatibility.

Backend:
- awx/conf/fields.py: switch URL TLD regex to use DomainNameValidator.ul in custom URLField.
- awx/main/management/commands/gather_analytics.py: use datetime.timezone.utc for naïve datetime handling.
- awx/main/dispatch/config.py: add mock_publish option; avoid DB access for test runs, set default max_workers, and support a noop broker.

Migrations (SQLite/Postgres compatibility):
- Add awx/main/migrations/_sqlite_helper.py with db-aware AlterIndexTogether/RenameIndex wrappers; consume in 0144_event_partitions.py and 0184_django_indexes.py.
- Update 0187_hop_nodes.py to use CheckConstraint(condition=...).
- Add 0205_alter_instance_peers_alter_job_hosts_and_more.py adjusting through_fields/relations on instance.peers, job.hosts, and role.ancestors.
- _dab_rbac.py: iterate roles with chunk_size=1000 for migration performance.

Tests:
Include hcp_terraform in default credential types in test_credential.py.
---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Alan Rominger <arominge@redhat.com>
This commit is contained in:
Hao Liu 2025-12-03 14:22:52 -05:00 committed by GitHub
parent 711b018ae7
commit b24156805a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 152 additions and 35 deletions

View File

@ -6,7 +6,7 @@ import urllib.parse as urlparse
from collections import OrderedDict
# Django
from django.core.validators import URLValidator, _lazy_re_compile
from django.core.validators import URLValidator, DomainNameValidator, _lazy_re_compile
from django.utils.translation import gettext_lazy as _
# Django REST Framework
@ -160,10 +160,11 @@ class StringListIsolatedPathField(StringListField):
class URLField(CharField):
# these lines set up a custom regex that allow numbers in the
# top-level domain
tld_re = (
r'\.' # dot
r'(?!-)' # can't start with a dash
r'(?:[a-z' + URLValidator.ul + r'0-9' + '-]{2,63}' # domain label, this line was changed from the original URLValidator
r'(?:[a-z' + DomainNameValidator.ul + r'0-9' + '-]{2,63}' # domain label, this line was changed from the original URLValidator
r'|xn--[a-z0-9]{1,59})' # or punycode label
r'(?<!-)' # can't end with a dash
r'\.?' # may have a trailing dot

View File

@ -11,13 +11,22 @@ def get_dispatcherd_config(for_service: bool = False, mock_publish: bool = False
Parameters:
for_service: if True, include dynamic options needed for running the dispatcher service
this will require database access, you should delay evaluation until after app setup
mock_publish: if True, use mock values that don't require database access
this is used during tests to avoid database queries during app initialization
"""
# When mock_publish=True (e.g., during tests), use a default value to avoid
# database access in get_auto_max_workers() which queries settings.IS_K8S
if mock_publish:
max_workers = 20 # Reasonable default for tests
else:
max_workers = get_auto_max_workers()
config = {
"version": 2,
"service": {
"pool_kwargs": {
"min_workers": settings.JOB_EVENT_WORKERS,
"max_workers": get_auto_max_workers(),
"max_workers": max_workers,
},
"main_kwargs": {"node_id": settings.CLUSTER_HOST_ID},
"process_manager_cls": "ForkServerManager",

View File

@ -1,9 +1,9 @@
import datetime
import logging
from awx.main import analytics
from dateutil import parser
from django.core.management.base import BaseCommand
from django.utils import timezone
class Command(BaseCommand):
@ -38,10 +38,10 @@ class Command(BaseCommand):
since = parser.parse(opt_since) if opt_since else None
if since and since.tzinfo is None:
since = since.replace(tzinfo=timezone.utc)
since = since.replace(tzinfo=datetime.timezone.utc)
until = parser.parse(opt_until) if opt_until else None
if until and until.tzinfo is None:
until = until.replace(tzinfo=timezone.utc)
until = until.replace(tzinfo=datetime.timezone.utc)
if opt_ship and opt_dry_run:
self.logger.error('Both --ship and --dry-run cannot be processed at the same time.')

View File

@ -237,7 +237,7 @@ class Migration(migrations.Migration):
db_index=False, editable=False, on_delete=models.deletion.DO_NOTHING, related_name='system_job_events', to='main.SystemJob'
),
),
migrations.AlterIndexTogether(
dbawaremigrations.AlterIndexTogether(
name='adhoccommandevent',
index_together={
('ad_hoc_command', 'job_created', 'event'),
@ -245,11 +245,11 @@ class Migration(migrations.Migration):
('ad_hoc_command', 'job_created', 'uuid'),
},
),
migrations.AlterIndexTogether(
dbawaremigrations.AlterIndexTogether(
name='inventoryupdateevent',
index_together={('inventory_update', 'job_created', 'counter'), ('inventory_update', 'job_created', 'uuid')},
),
migrations.AlterIndexTogether(
dbawaremigrations.AlterIndexTogether(
name='jobevent',
index_together={
('job', 'job_created', 'counter'),
@ -258,7 +258,7 @@ class Migration(migrations.Migration):
('job', 'job_created', 'parent_uuid'),
},
),
migrations.AlterIndexTogether(
dbawaremigrations.AlterIndexTogether(
name='projectupdateevent',
index_together={
('project_update', 'job_created', 'uuid'),
@ -266,7 +266,7 @@ class Migration(migrations.Migration):
('project_update', 'job_created', 'counter'),
},
),
migrations.AlterIndexTogether(
dbawaremigrations.AlterIndexTogether(
name='systemjobevent',
index_together={('system_job', 'job_created', 'uuid'), ('system_job', 'job_created', 'counter')},
),

View File

@ -6,6 +6,8 @@ from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
from ._sqlite_helper import dbawaremigrations
class Migration(migrations.Migration):
dependencies = [
@ -15,92 +17,92 @@ class Migration(migrations.Migration):
]
operations = [
migrations.RenameIndex(
dbawaremigrations.RenameIndex(
model_name='adhoccommandevent',
new_name='main_adhocc_ad_hoc__1e4d24_idx',
old_fields=('ad_hoc_command', 'job_created', 'uuid'),
),
migrations.RenameIndex(
dbawaremigrations.RenameIndex(
model_name='adhoccommandevent',
new_name='main_adhocc_ad_hoc__e72142_idx',
old_fields=('ad_hoc_command', 'job_created', 'event'),
),
migrations.RenameIndex(
dbawaremigrations.RenameIndex(
model_name='adhoccommandevent',
new_name='main_adhocc_ad_hoc__a57777_idx',
old_fields=('ad_hoc_command', 'job_created', 'counter'),
),
migrations.RenameIndex(
dbawaremigrations.RenameIndex(
model_name='inventoryupdateevent',
new_name='main_invent_invento_f72b21_idx',
old_fields=('inventory_update', 'job_created', 'uuid'),
),
migrations.RenameIndex(
dbawaremigrations.RenameIndex(
model_name='inventoryupdateevent',
new_name='main_invent_invento_364dcb_idx',
old_fields=('inventory_update', 'job_created', 'counter'),
),
migrations.RenameIndex(
dbawaremigrations.RenameIndex(
model_name='jobevent',
new_name='main_jobeve_job_id_40a56d_idx',
old_fields=('job', 'job_created', 'parent_uuid'),
),
migrations.RenameIndex(
dbawaremigrations.RenameIndex(
model_name='jobevent',
new_name='main_jobeve_job_id_3c4a4a_idx',
old_fields=('job', 'job_created', 'uuid'),
),
migrations.RenameIndex(
dbawaremigrations.RenameIndex(
model_name='jobevent',
new_name='main_jobeve_job_id_51c382_idx',
old_fields=('job', 'job_created', 'counter'),
),
migrations.RenameIndex(
dbawaremigrations.RenameIndex(
model_name='jobevent',
new_name='main_jobeve_job_id_0ddc6b_idx',
old_fields=('job', 'job_created', 'event'),
),
migrations.RenameIndex(
dbawaremigrations.RenameIndex(
model_name='projectupdateevent',
new_name='main_projec_project_449bbd_idx',
old_fields=('project_update', 'job_created', 'uuid'),
),
migrations.RenameIndex(
dbawaremigrations.RenameIndex(
model_name='projectupdateevent',
new_name='main_projec_project_69559a_idx',
old_fields=('project_update', 'job_created', 'counter'),
),
migrations.RenameIndex(
dbawaremigrations.RenameIndex(
model_name='projectupdateevent',
new_name='main_projec_project_c44b7c_idx',
old_fields=('project_update', 'job_created', 'event'),
),
migrations.RenameIndex(
dbawaremigrations.RenameIndex(
model_name='role',
new_name='main_rbac_r_content_979bdd_idx',
old_fields=('content_type', 'object_id'),
),
migrations.RenameIndex(
dbawaremigrations.RenameIndex(
model_name='roleancestorentry',
new_name='main_rbac_r_ancesto_b44606_idx',
old_fields=('ancestor', 'content_type_id', 'role_field'),
),
migrations.RenameIndex(
dbawaremigrations.RenameIndex(
model_name='roleancestorentry',
new_name='main_rbac_r_ancesto_22b9f0_idx',
old_fields=('ancestor', 'content_type_id', 'object_id'),
),
migrations.RenameIndex(
dbawaremigrations.RenameIndex(
model_name='roleancestorentry',
new_name='main_rbac_r_ancesto_c87b87_idx',
old_fields=('ancestor', 'descendent'),
),
migrations.RenameIndex(
dbawaremigrations.RenameIndex(
model_name='systemjobevent',
new_name='main_system_system__e39825_idx',
old_fields=('system_job', 'job_created', 'uuid'),
),
migrations.RenameIndex(
dbawaremigrations.RenameIndex(
model_name='systemjobevent',
new_name='main_system_system__73537a_idx',
old_fields=('system_job', 'job_created', 'counter'),

View File

@ -69,7 +69,7 @@ class Migration(migrations.Migration):
),
migrations.AddConstraint(
model_name='instancelink',
constraint=models.CheckConstraint(check=models.Q(('source', models.F('target')), _negated=True), name='source_and_target_can_not_be_equal'),
constraint=models.CheckConstraint(condition=models.Q(('source', models.F('target')), _negated=True), name='source_and_target_can_not_be_equal'),
),
migrations.RunPython(automatically_peer_from_control_plane),
]

View File

@ -0,0 +1,32 @@
# Generated by Django 5.2.8 on 2025-11-20 18:39
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('main', '0204_squashed_deletions'),
]
operations = [
migrations.AlterField(
model_name='instance',
name='peers',
field=models.ManyToManyField(
related_name='peers_from', through='main.InstanceLink', through_fields=('source', 'target'), to='main.receptoraddress'
),
),
migrations.AlterField(
model_name='job',
name='hosts',
field=models.ManyToManyField(editable=False, related_name='jobs', through='main.JobHostSummary', through_fields=('job', 'host'), to='main.host'),
),
migrations.AlterField(
model_name='role',
name='ancestors',
field=models.ManyToManyField(
related_name='descendents', through='main.RoleAncestorEntry', through_fields=('descendent', 'ancestor'), to='main.role'
),
),
]

View File

@ -194,7 +194,7 @@ def migrate_to_new_rbac(apps, schema_editor):
# NOTE: this import is expected to break at some point, and then just move the data here
from awx.main.models.rbac import role_descriptions
for role in Role.objects.prefetch_related('members', 'parents').iterator():
for role in Role.objects.prefetch_related('members', 'parents').iterator(chunk_size=1000):
if role.singleton_name:
continue # only bothering to migrate object roles

View File

@ -1,6 +1,78 @@
from django.db import migrations
class AlterIndexTogether(migrations.AlterIndexTogether):
"""
Database-aware AlterIndexTogether that handles SQLite's missing indexes gracefully.
In Django 5.2+, SQLite table rewrites (triggered by AlterField operations)
can drop multi-column indexes. For SQLite, this catches the ValueError and
ignores it when the index doesn't exist. For PostgreSQL, uses standard behavior.
"""
def database_forwards(self, app_label, schema_editor, from_state, to_state):
if not schema_editor.connection.vendor.startswith('postgres'):
# SQLite-specific handling: ignore missing indexes from table rewrites
try:
super().database_forwards(app_label, schema_editor, from_state, to_state)
except ValueError as exc:
if "Found wrong number (0) of constraints" in str(exc) or "Found wrong number (0) of indexes" in str(exc):
return
raise
else:
# PostgreSQL: standard behavior
super().database_forwards(app_label, schema_editor, from_state, to_state)
def database_backwards(self, app_label, schema_editor, from_state, to_state):
if not schema_editor.connection.vendor.startswith('postgres'):
# SQLite-specific handling: ignore missing indexes from table rewrites
try:
super().database_backwards(app_label, schema_editor, from_state, to_state)
except ValueError as exc:
if "Found wrong number (0) of constraints" in str(exc) or "Found wrong number (0) of indexes" in str(exc):
return
raise
else:
# PostgreSQL: standard behavior
super().database_backwards(app_label, schema_editor, from_state, to_state)
class RenameIndex(migrations.RenameIndex):
"""
Database-aware RenameIndex that handles SQLite's missing indexes gracefully.
In Django 5.2+, SQLite table rewrites (triggered by AlterField operations)
can drop multi-column indexes. For SQLite, this catches the ValueError and
ignores it when the index doesn't exist. For PostgreSQL, uses standard behavior.
"""
def database_forwards(self, app_label, schema_editor, from_state, to_state):
if not schema_editor.connection.vendor.startswith('postgres'):
# SQLite-specific handling: ignore missing indexes from table rewrites
try:
super().database_forwards(app_label, schema_editor, from_state, to_state)
except ValueError as exc:
if "Found wrong number (0) of constraints" in str(exc) or "wrong number (0) of indexes" in str(exc):
return
raise
else:
# PostgreSQL: standard behavior
super().database_forwards(app_label, schema_editor, from_state, to_state)
def database_backwards(self, app_label, schema_editor, from_state, to_state):
if not schema_editor.connection.vendor.startswith('postgres'):
# SQLite-specific handling: ignore missing indexes from table rewrites
try:
super().database_backwards(app_label, schema_editor, from_state, to_state)
except ValueError as exc:
if "Found wrong number (0) of constraints" in str(exc) or "wrong number (0) of indexes" in str(exc):
return
raise
else:
# PostgreSQL: standard behavior
super().database_backwards(app_label, schema_editor, from_state, to_state)
class RunSQL(migrations.operations.special.RunSQL):
"""
Bit of a hack here. Django actually wants this decision made in the router
@ -56,6 +128,8 @@ class RunPython(migrations.operations.special.RunPython):
class _sqlitemigrations:
RunPython = RunPython
RunSQL = RunSQL
AlterIndexTogether = AlterIndexTogether
RenameIndex = RenameIndex
dbawaremigrations = _sqlitemigrations()

View File

@ -13,7 +13,7 @@ cryptography
Cython
daphne
distro
django==4.2.26 # CVE-2025-32873
django>=5.2,<5.3 # Django 5.2 LTS, allow patch updates
django-cors-headers
django-crum
django-extensions

View File

@ -124,7 +124,7 @@ dispatcherd==2025.5.21
# via -r /awx_devel/requirements/requirements.in
distro==1.9.0
# via -r /awx_devel/requirements/requirements.in
django==4.2.26
django==5.2.8
# via
# -r /awx_devel/requirements/requirements.in
# channels

View File

@ -1,5 +1,5 @@
build
django-debug-toolbar==3.2.4
django-debug-toolbar>=6.0 # Django 5.2 compatibility
django-test-migrations
drf-spectacular>=0.27.0 # Modern OpenAPI 3.0 schema generator
# pprofile - re-add once https://github.com/vpelletier/pprofile/issues/41 is addressed
@ -28,4 +28,3 @@ pip>=21.3,<=24.0 # PEP 660 Editable installs for pyproject.toml based builds
debugpy
remote-pdb
sdb