mirror of
https://github.com/ansible/awx.git
synced 2026-07-21 05:08:14 -02:30
fix failing test
This commit is contained in:
@@ -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()
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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):
|
||||
|
||||
Reference in New Issue
Block a user