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
from ansible_base.lib.utils.validation import to_python_boolean
from ansible_base.rbac.models import RoleEvaluation
from ansible_base.rbac.policies import visible_users
from ansible_base.rbac import permission_registry
# AWX
@@ -643,6 +644,8 @@ class UserAccess(BaseAccess):
Organization.access_qs(self.user, 'change').exists() or Organization.access_qs(self.user, 'audit').exists()
):
qs = User.objects.all()
elif settings.ANSIBLE_BASE_ROLE_SYSTEM_ACTIVATED:
qs = visible_users(self.user)
else:
qs = (
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
return False
if settings.ANSIBLE_BASE_ROLE_SYSTEM_ACTIVATED:
# Permission granted if the user has all permissions that the target user has
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(
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 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)
# then delete the inventory itself
inv_id = inventory.id
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)
@@ -1062,13 +1063,12 @@ def delete_inventory(inventory_id, user_id, retries=5):
with ignore_inventory_computed_fields(), ignore_inventory_group_removal(), impersonate(user):
try:
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)
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))
except Inventory.DoesNotExist:
logger.warning("Delete Inventory failed due to missing inventory: " + str(inventory_id))
return
except DatabaseError:
logger.exception('Database error deleting inventory {}, but will retry.'.format(inventory_id))
if retries > 0:

View File

@@ -16,6 +16,8 @@ from awx.main.tasks.system import (
_batched_delete_inventory,
)
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.inventory import Group, Host
@@ -115,10 +117,13 @@ def test_folder_cleanup_multiple_running_jobs(job_folder_factory, me_inst):
@pytest.mark.django_db
class TestBatchedDeleteInventory:
def _make_inventory_with_hosts(self, count):
from django.utils import timezone
now = timezone.now()
org = Organization.objects.create(name='test-org')
inv = Inventory.objects.create(name='test-inv', organization=org)
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)
group.hosts.set(Host.objects.filter(inventory=inv))
return inv
@@ -144,6 +149,47 @@ class TestBatchedDeleteInventory:
assert not Host.objects.filter(inventory_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
def test_clear_setting_cache_log_level_branch(settings):