fix failing test

This commit is contained in:
Peter Braun
2026-07-17 14:48:54 +02:00
parent 1603d4513a
commit 3f5c03a4ee
3 changed files with 59 additions and 9 deletions

View File

@@ -20,6 +20,7 @@ from rest_framework.exceptions import ParseError, PermissionDenied
# django-ansible-base # django-ansible-base
from ansible_base.lib.utils.validation import to_python_boolean from ansible_base.lib.utils.validation import to_python_boolean
from ansible_base.rbac.models import RoleEvaluation from ansible_base.rbac.models import RoleEvaluation
from ansible_base.rbac.policies import visible_users
from ansible_base.rbac import permission_registry from ansible_base.rbac import permission_registry
# AWX # AWX
@@ -643,6 +644,8 @@ class UserAccess(BaseAccess):
Organization.access_qs(self.user, 'change').exists() or Organization.access_qs(self.user, 'audit').exists() Organization.access_qs(self.user, 'change').exists() or Organization.access_qs(self.user, 'audit').exists()
): ):
qs = User.objects.all() qs = User.objects.all()
elif settings.ANSIBLE_BASE_ROLE_SYSTEM_ACTIVATED:
qs = visible_users(self.user)
else: else:
qs = ( qs = (
User.objects.filter(pk__in=Organization.access_qs(self.user, 'view').values('member_role__members')) User.objects.filter(pk__in=Organization.access_qs(self.user, 'view').values('member_role__members'))
@@ -706,12 +709,13 @@ class UserAccess(BaseAccess):
# in these cases only superusers can modify orphan users # in these cases only superusers can modify orphan users
return False return False
if settings.ANSIBLE_BASE_ROLE_SYSTEM_ACTIVATED: if settings.ANSIBLE_BASE_ROLE_SYSTEM_ACTIVATED:
# Permission granted if the user has all permissions that the target user has
target_perms = set( target_perms = set(
RoleEvaluation.objects.filter(role__in=obj.has_roles.all()).values_list('object_id', 'content_type_id', 'codename').distinct() RoleEvaluation.objects.filter(**RoleEvaluation._actor_role_filter(obj)).values_list('object_id', 'content_type_id', 'codename').distinct()
) )
user_perms = set( user_perms = set(
RoleEvaluation.objects.filter(role__in=self.user.has_roles.all()).values_list('object_id', 'content_type_id', 'codename').distinct() RoleEvaluation.objects.filter(**RoleEvaluation._actor_role_filter(self.user))
.values_list('object_id', 'content_type_id', 'codename')
.distinct()
) )
return not (target_perms - user_perms) return not (target_perms - user_perms)
return not obj.roles.all().exclude(ancestors__in=self.user.roles.all()).exists() return not obj.roles.all().exclude(ancestors__in=self.user.roles.all()).exists()

View File

@@ -1045,8 +1045,9 @@ def _batched_delete_inventory(inventory, batch_size=500):
logger.debug('Batch-deleted %d hosts from inventory %d (%d total so far)', len(pks), inventory.id, total_deleted) logger.debug('Batch-deleted %d hosts from inventory %d (%d total so far)', len(pks), inventory.id, total_deleted)
# then delete the inventory itself # then delete the inventory itself
inv_id = inventory.id
inventory.delete() inventory.delete()
logger.info('Batched deletion of inventory %d complete (%d hosts removed)', inventory.id, total_deleted) logger.info('Batched deletion of inventory %d complete (%d hosts removed)', inv_id, total_deleted)
@task(queue=get_task_queuename, timeout=3600 * 5) @task(queue=get_task_queuename, timeout=3600 * 5)
@@ -1062,13 +1063,12 @@ def delete_inventory(inventory_id, user_id, retries=5):
with ignore_inventory_computed_fields(), ignore_inventory_group_removal(), impersonate(user): with ignore_inventory_computed_fields(), ignore_inventory_group_removal(), impersonate(user):
try: try:
inv = Inventory.objects.get(id=inventory_id) inv = Inventory.objects.get(id=inventory_id)
except Inventory.DoesNotExist:
logger.exception("Delete Inventory failed due to missing inventory: " + str(inventory_id))
return
try:
_batched_delete_inventory(inv) _batched_delete_inventory(inv)
emit_channel_notification('inventories-status_changed', {'group_name': 'inventories', 'inventory_id': inventory_id, 'status': 'deleted'}) emit_channel_notification('inventories-status_changed', {'group_name': 'inventories', 'inventory_id': inventory_id, 'status': 'deleted'})
logger.debug('Deleted inventory {} as user {}.'.format(inventory_id, user_id)) logger.debug('Deleted inventory {} as user {}.'.format(inventory_id, user_id))
except Inventory.DoesNotExist:
logger.warning("Delete Inventory failed due to missing inventory: " + str(inventory_id))
return
except DatabaseError: except DatabaseError:
logger.exception('Database error deleting inventory {}, but will retry.'.format(inventory_id)) logger.exception('Database error deleting inventory {}, but will retry.'.format(inventory_id))
if retries > 0: if retries > 0:

View File

@@ -16,6 +16,8 @@ from awx.main.tasks.system import (
_batched_delete_inventory, _batched_delete_inventory,
) )
from awx.main.management.commands.dispatcherd import Command from awx.main.management.commands.dispatcherd import Command
from django.db import DatabaseError
from awx.main.models import Instance, Inventory, Job, Organization, ReceptorAddress, InstanceLink from awx.main.models import Instance, Inventory, Job, Organization, ReceptorAddress, InstanceLink
from awx.main.models.inventory import Group, Host from awx.main.models.inventory import Group, Host
@@ -115,10 +117,13 @@ def test_folder_cleanup_multiple_running_jobs(job_folder_factory, me_inst):
@pytest.mark.django_db @pytest.mark.django_db
class TestBatchedDeleteInventory: class TestBatchedDeleteInventory:
def _make_inventory_with_hosts(self, count): def _make_inventory_with_hosts(self, count):
from django.utils import timezone
now = timezone.now()
org = Organization.objects.create(name='test-org') org = Organization.objects.create(name='test-org')
inv = Inventory.objects.create(name='test-inv', organization=org) inv = Inventory.objects.create(name='test-inv', organization=org)
group = Group.objects.create(name='test-group', inventory=inv) group = Group.objects.create(name='test-group', inventory=inv)
hosts = [Host(name=f'host-{i}', inventory=inv) for i in range(count)] hosts = [Host(name=f'host-{i}', inventory=inv, created=now, modified=now) for i in range(count)]
Host.objects.bulk_create(hosts) Host.objects.bulk_create(hosts)
group.hosts.set(Host.objects.filter(inventory=inv)) group.hosts.set(Host.objects.filter(inventory=inv))
return inv return inv
@@ -144,6 +149,47 @@ class TestBatchedDeleteInventory:
assert not Host.objects.filter(inventory_id=inv_id).exists() assert not Host.objects.filter(inventory_id=inv_id).exists()
assert not Inventory.objects.filter(id=inv_id).exists() assert not Inventory.objects.filter(id=inv_id).exists()
def test_idempotent_after_partial_delete(self):
"""Simulate a crash mid-way: delete some hosts manually, then run
_batched_delete_inventory — it should finish the job cleanly."""
inv = self._make_inventory_with_hosts(10)
inv_id = inv.id
# Simulate a partial deletion (as if the task crashed after 4 hosts)
partial_pks = list(Host.objects.filter(inventory=inv).values_list('pk', flat=True)[:4])
Host.objects.filter(pk__in=partial_pks).delete()
assert Host.objects.filter(inventory_id=inv_id).count() == 6
# Re-running should delete the remaining hosts and the inventory
inv.refresh_from_db()
_batched_delete_inventory(inv, batch_size=3)
assert not Host.objects.filter(inventory_id=inv_id).exists()
assert not Inventory.objects.filter(id=inv_id).exists()
def test_delete_inventory_retries_on_database_error(self):
"""DatabaseError during deletion triggers a retry."""
from awx.main.tasks.system import delete_inventory
inv = self._make_inventory_with_hosts(3)
inv_id = inv.id
call_count = {'n': 0}
original = _batched_delete_inventory.__wrapped__ if hasattr(_batched_delete_inventory, '__wrapped__') else _batched_delete_inventory
def flaky_delete(inventory, batch_size=500):
call_count['n'] += 1
if call_count['n'] == 1:
raise DatabaseError('connection reset')
return original(inventory, batch_size=batch_size)
with mock.patch('awx.main.tasks.system._batched_delete_inventory', side_effect=flaky_delete):
with mock.patch('awx.main.tasks.system.emit_channel_notification'):
with mock.patch('awx.main.tasks.system.time.sleep'):
delete_inventory(inv_id, None, retries=2)
assert call_count['n'] == 2
assert not Inventory.objects.filter(id=inv_id).exists()
@pytest.mark.django_db @pytest.mark.django_db
def test_clear_setting_cache_log_level_branch(settings): def test_clear_setting_cache_log_level_branch(settings):