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
12 changed files with 152 additions and 35 deletions

View File

@@ -6,7 +6,7 @@ import urllib.parse as urlparse
from collections import OrderedDict from collections import OrderedDict
# Django # 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 _ from django.utils.translation import gettext_lazy as _
# Django REST Framework # Django REST Framework
@@ -160,10 +160,11 @@ class StringListIsolatedPathField(StringListField):
class URLField(CharField): class URLField(CharField):
# these lines set up a custom regex that allow numbers in the # these lines set up a custom regex that allow numbers in the
# top-level domain # top-level domain
tld_re = ( tld_re = (
r'\.' # dot r'\.' # dot
r'(?!-)' # can't start with a dash 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'|xn--[a-z0-9]{1,59})' # or punycode label
r'(?<!-)' # can't end with a dash r'(?<!-)' # can't end with a dash
r'\.?' # may have a trailing dot 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: Parameters:
for_service: if True, include dynamic options needed for running the dispatcher service 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 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 = { config = {
"version": 2, "version": 2,
"service": { "service": {
"pool_kwargs": { "pool_kwargs": {
"min_workers": settings.JOB_EVENT_WORKERS, "min_workers": settings.JOB_EVENT_WORKERS,
"max_workers": get_auto_max_workers(), "max_workers": max_workers,
}, },
"main_kwargs": {"node_id": settings.CLUSTER_HOST_ID}, "main_kwargs": {"node_id": settings.CLUSTER_HOST_ID},
"process_manager_cls": "ForkServerManager", "process_manager_cls": "ForkServerManager",

View File

@@ -1,9 +1,9 @@
import datetime
import logging import logging
from awx.main import analytics from awx.main import analytics
from dateutil import parser from dateutil import parser
from django.core.management.base import BaseCommand from django.core.management.base import BaseCommand
from django.utils import timezone
class Command(BaseCommand): class Command(BaseCommand):
@@ -38,10 +38,10 @@ class Command(BaseCommand):
since = parser.parse(opt_since) if opt_since else None since = parser.parse(opt_since) if opt_since else None
if since and since.tzinfo is 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 until = parser.parse(opt_until) if opt_until else None
if until and until.tzinfo is 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: if opt_ship and opt_dry_run:
self.logger.error('Both --ship and --dry-run cannot be processed at the same time.') 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' 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', name='adhoccommandevent',
index_together={ index_together={
('ad_hoc_command', 'job_created', 'event'), ('ad_hoc_command', 'job_created', 'event'),
@@ -245,11 +245,11 @@ class Migration(migrations.Migration):
('ad_hoc_command', 'job_created', 'uuid'), ('ad_hoc_command', 'job_created', 'uuid'),
}, },
), ),
migrations.AlterIndexTogether( dbawaremigrations.AlterIndexTogether(
name='inventoryupdateevent', name='inventoryupdateevent',
index_together={('inventory_update', 'job_created', 'counter'), ('inventory_update', 'job_created', 'uuid')}, index_together={('inventory_update', 'job_created', 'counter'), ('inventory_update', 'job_created', 'uuid')},
), ),
migrations.AlterIndexTogether( dbawaremigrations.AlterIndexTogether(
name='jobevent', name='jobevent',
index_together={ index_together={
('job', 'job_created', 'counter'), ('job', 'job_created', 'counter'),
@@ -258,7 +258,7 @@ class Migration(migrations.Migration):
('job', 'job_created', 'parent_uuid'), ('job', 'job_created', 'parent_uuid'),
}, },
), ),
migrations.AlterIndexTogether( dbawaremigrations.AlterIndexTogether(
name='projectupdateevent', name='projectupdateevent',
index_together={ index_together={
('project_update', 'job_created', 'uuid'), ('project_update', 'job_created', 'uuid'),
@@ -266,7 +266,7 @@ class Migration(migrations.Migration):
('project_update', 'job_created', 'counter'), ('project_update', 'job_created', 'counter'),
}, },
), ),
migrations.AlterIndexTogether( dbawaremigrations.AlterIndexTogether(
name='systemjobevent', name='systemjobevent',
index_together={('system_job', 'job_created', 'uuid'), ('system_job', 'job_created', 'counter')}, 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 from django.db import migrations, models
import django.db.models.deletion import django.db.models.deletion
from ._sqlite_helper import dbawaremigrations
class Migration(migrations.Migration): class Migration(migrations.Migration):
dependencies = [ dependencies = [
@@ -15,92 +17,92 @@ class Migration(migrations.Migration):
] ]
operations = [ operations = [
migrations.RenameIndex( dbawaremigrations.RenameIndex(
model_name='adhoccommandevent', model_name='adhoccommandevent',
new_name='main_adhocc_ad_hoc__1e4d24_idx', new_name='main_adhocc_ad_hoc__1e4d24_idx',
old_fields=('ad_hoc_command', 'job_created', 'uuid'), old_fields=('ad_hoc_command', 'job_created', 'uuid'),
), ),
migrations.RenameIndex( dbawaremigrations.RenameIndex(
model_name='adhoccommandevent', model_name='adhoccommandevent',
new_name='main_adhocc_ad_hoc__e72142_idx', new_name='main_adhocc_ad_hoc__e72142_idx',
old_fields=('ad_hoc_command', 'job_created', 'event'), old_fields=('ad_hoc_command', 'job_created', 'event'),
), ),
migrations.RenameIndex( dbawaremigrations.RenameIndex(
model_name='adhoccommandevent', model_name='adhoccommandevent',
new_name='main_adhocc_ad_hoc__a57777_idx', new_name='main_adhocc_ad_hoc__a57777_idx',
old_fields=('ad_hoc_command', 'job_created', 'counter'), old_fields=('ad_hoc_command', 'job_created', 'counter'),
), ),
migrations.RenameIndex( dbawaremigrations.RenameIndex(
model_name='inventoryupdateevent', model_name='inventoryupdateevent',
new_name='main_invent_invento_f72b21_idx', new_name='main_invent_invento_f72b21_idx',
old_fields=('inventory_update', 'job_created', 'uuid'), old_fields=('inventory_update', 'job_created', 'uuid'),
), ),
migrations.RenameIndex( dbawaremigrations.RenameIndex(
model_name='inventoryupdateevent', model_name='inventoryupdateevent',
new_name='main_invent_invento_364dcb_idx', new_name='main_invent_invento_364dcb_idx',
old_fields=('inventory_update', 'job_created', 'counter'), old_fields=('inventory_update', 'job_created', 'counter'),
), ),
migrations.RenameIndex( dbawaremigrations.RenameIndex(
model_name='jobevent', model_name='jobevent',
new_name='main_jobeve_job_id_40a56d_idx', new_name='main_jobeve_job_id_40a56d_idx',
old_fields=('job', 'job_created', 'parent_uuid'), old_fields=('job', 'job_created', 'parent_uuid'),
), ),
migrations.RenameIndex( dbawaremigrations.RenameIndex(
model_name='jobevent', model_name='jobevent',
new_name='main_jobeve_job_id_3c4a4a_idx', new_name='main_jobeve_job_id_3c4a4a_idx',
old_fields=('job', 'job_created', 'uuid'), old_fields=('job', 'job_created', 'uuid'),
), ),
migrations.RenameIndex( dbawaremigrations.RenameIndex(
model_name='jobevent', model_name='jobevent',
new_name='main_jobeve_job_id_51c382_idx', new_name='main_jobeve_job_id_51c382_idx',
old_fields=('job', 'job_created', 'counter'), old_fields=('job', 'job_created', 'counter'),
), ),
migrations.RenameIndex( dbawaremigrations.RenameIndex(
model_name='jobevent', model_name='jobevent',
new_name='main_jobeve_job_id_0ddc6b_idx', new_name='main_jobeve_job_id_0ddc6b_idx',
old_fields=('job', 'job_created', 'event'), old_fields=('job', 'job_created', 'event'),
), ),
migrations.RenameIndex( dbawaremigrations.RenameIndex(
model_name='projectupdateevent', model_name='projectupdateevent',
new_name='main_projec_project_449bbd_idx', new_name='main_projec_project_449bbd_idx',
old_fields=('project_update', 'job_created', 'uuid'), old_fields=('project_update', 'job_created', 'uuid'),
), ),
migrations.RenameIndex( dbawaremigrations.RenameIndex(
model_name='projectupdateevent', model_name='projectupdateevent',
new_name='main_projec_project_69559a_idx', new_name='main_projec_project_69559a_idx',
old_fields=('project_update', 'job_created', 'counter'), old_fields=('project_update', 'job_created', 'counter'),
), ),
migrations.RenameIndex( dbawaremigrations.RenameIndex(
model_name='projectupdateevent', model_name='projectupdateevent',
new_name='main_projec_project_c44b7c_idx', new_name='main_projec_project_c44b7c_idx',
old_fields=('project_update', 'job_created', 'event'), old_fields=('project_update', 'job_created', 'event'),
), ),
migrations.RenameIndex( dbawaremigrations.RenameIndex(
model_name='role', model_name='role',
new_name='main_rbac_r_content_979bdd_idx', new_name='main_rbac_r_content_979bdd_idx',
old_fields=('content_type', 'object_id'), old_fields=('content_type', 'object_id'),
), ),
migrations.RenameIndex( dbawaremigrations.RenameIndex(
model_name='roleancestorentry', model_name='roleancestorentry',
new_name='main_rbac_r_ancesto_b44606_idx', new_name='main_rbac_r_ancesto_b44606_idx',
old_fields=('ancestor', 'content_type_id', 'role_field'), old_fields=('ancestor', 'content_type_id', 'role_field'),
), ),
migrations.RenameIndex( dbawaremigrations.RenameIndex(
model_name='roleancestorentry', model_name='roleancestorentry',
new_name='main_rbac_r_ancesto_22b9f0_idx', new_name='main_rbac_r_ancesto_22b9f0_idx',
old_fields=('ancestor', 'content_type_id', 'object_id'), old_fields=('ancestor', 'content_type_id', 'object_id'),
), ),
migrations.RenameIndex( dbawaremigrations.RenameIndex(
model_name='roleancestorentry', model_name='roleancestorentry',
new_name='main_rbac_r_ancesto_c87b87_idx', new_name='main_rbac_r_ancesto_c87b87_idx',
old_fields=('ancestor', 'descendent'), old_fields=('ancestor', 'descendent'),
), ),
migrations.RenameIndex( dbawaremigrations.RenameIndex(
model_name='systemjobevent', model_name='systemjobevent',
new_name='main_system_system__e39825_idx', new_name='main_system_system__e39825_idx',
old_fields=('system_job', 'job_created', 'uuid'), old_fields=('system_job', 'job_created', 'uuid'),
), ),
migrations.RenameIndex( dbawaremigrations.RenameIndex(
model_name='systemjobevent', model_name='systemjobevent',
new_name='main_system_system__73537a_idx', new_name='main_system_system__73537a_idx',
old_fields=('system_job', 'job_created', 'counter'), old_fields=('system_job', 'job_created', 'counter'),

View File

@@ -69,7 +69,7 @@ class Migration(migrations.Migration):
), ),
migrations.AddConstraint( migrations.AddConstraint(
model_name='instancelink', 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), 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 # 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 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: if role.singleton_name:
continue # only bothering to migrate object roles continue # only bothering to migrate object roles

View File

@@ -1,6 +1,78 @@
from django.db import migrations 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): class RunSQL(migrations.operations.special.RunSQL):
""" """
Bit of a hack here. Django actually wants this decision made in the router 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: class _sqlitemigrations:
RunPython = RunPython RunPython = RunPython
RunSQL = RunSQL RunSQL = RunSQL
AlterIndexTogether = AlterIndexTogether
RenameIndex = RenameIndex
dbawaremigrations = _sqlitemigrations() dbawaremigrations = _sqlitemigrations()

View File

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

View File

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

View File

@@ -1,5 +1,5 @@
build build
django-debug-toolbar==3.2.4 django-debug-toolbar>=6.0 # Django 5.2 compatibility
django-test-migrations django-test-migrations
drf-spectacular>=0.27.0 # Modern OpenAPI 3.0 schema generator drf-spectacular>=0.27.0 # Modern OpenAPI 3.0 schema generator
# pprofile - re-add once https://github.com/vpelletier/pprofile/issues/41 is addressed # 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 debugpy
remote-pdb remote-pdb
sdb sdb