fix: address comments

This commit is contained in:
Peter Braun
2026-04-28 15:12:13 +02:00
parent 7cacacbd32
commit 17a1a20910
4 changed files with 77 additions and 3 deletions

View File

@@ -209,9 +209,10 @@ class DashboardView(APIView):
groups_inventory_failed = models.Group.objects.filter(inventory_sources__last_job_failed=True).count()
data['groups'] = {'url': reverse('api:group_list', request=request), 'total': user_groups.count(), 'inventory_failed': groups_inventory_failed}
user_hosts = get_user_queryset(request.user, models.Host)
user_hosts = get_user_queryset(request.user, models.Host).exclude(inventory__kind='constructed')
latest_summary_failed = Subquery(models.JobHostSummary.objects.filter(host_id=OuterRef('pk')).order_by('-id').values('failed')[:1])
user_hosts_failed = user_hosts.annotate(_latest_failed=latest_summary_failed).filter(_latest_failed=True)
data['hosts'] = {
'url': reverse('api:host_list', request=request),
'total': user_hosts.count(),

View File

@@ -90,23 +90,40 @@ class HostManager(models.Manager.from_queryset(HostLatestSummaryQuerySet)):
Construction of query involves:
- remove any ordering specified in model's Meta
- Exclude hosts sourced from another Tower
- Exclude hosts in constructed inventories (these are shadow rows of source-inventory hosts)
- Restrict the query to only return the name column
- Only consider results that are unique
- Return the count of this query
"""
return self.order_by().exclude(inventory_sources__source='controller').values(name_lower=Lower('name')).distinct().count()
return (
self.order_by()
.exclude(inventory_sources__source='controller')
.exclude(inventory__kind='constructed')
.values(name_lower=Lower('name'))
.distinct()
.count()
)
def org_active_count(self, org_id):
"""Return count of active, unique hosts used by an organization.
Construction of query involves:
- remove any ordering specified in model's Meta
- Exclude hosts sourced from another Tower
- Exclude hosts in constructed inventories (these are shadow rows of source-inventory hosts)
- Consider only hosts where the canonical inventory is owned by the organization
- Restrict the query to only return the name column
- Only consider results that are unique
- Return the count of this query
"""
return self.order_by().exclude(inventory_sources__source='controller').filter(inventory__organization=org_id).values('name').distinct().count()
return (
self.order_by()
.exclude(inventory_sources__source='controller')
.exclude(inventory__kind='constructed')
.filter(inventory__organization=org_id)
.values('name')
.distinct()
.count()
)
def get_queryset(self):
"""When the parent instance of the host query set has a `kind=smart` and a `host_filter`

View File

@@ -0,0 +1,34 @@
import pytest
from awx.api.versioning import reverse
from awx.main.models import Host, Inventory
@pytest.mark.django_db
def test_dashboard_hosts_total_excludes_constructed(get, admin_user, organization):
"""
Constructed inventory hosts are not counted in the dashboard
"""
source_inv = Inventory.objects.create(name='source-inv', organization=organization)
source_host = source_inv.hosts.create(name='host1')
constructed = Inventory.objects.create(name='constructed-inv', kind='constructed', organization=organization)
Host.objects.create(name='host1', inventory=constructed, instance_id=str(source_host.pk))
response = get(reverse('api:dashboard_view'), user=admin_user, expect=200)
assert response.data['hosts']['total'] == 1
@pytest.mark.django_db
def test_host_list_still_returns_constructed(get, admin_user, organization):
"""
Constructed inventory hosts are still visible through the API
"""
source_inv = Inventory.objects.create(name='source-inv', organization=organization)
source_host = source_inv.hosts.create(name='host1')
constructed = Inventory.objects.create(name='constructed-inv', kind='constructed', organization=organization)
Host.objects.create(name='host1', inventory=constructed, instance_id=str(source_host.pk))
response = get(reverse('api:host_list'), user=admin_user, expect=200)
assert response.data['count'] == 2

View File

@@ -108,6 +108,28 @@ class TestActiveCount:
source.hosts.create(name='remotely-managed-host', inventory=inventory)
assert Host.objects.active_count() == 1
def test_active_count_minus_constructed(self, organization):
"""
Active hosts do not include duplicated hosts from construted inventories.
"""
inv = Inventory.objects.create(name='source-inv', organization=organization)
inv.hosts.create(name='host1')
assert Host.objects.active_count() == 1
constructed = Inventory.objects.create(name='constructed-inv', kind='constructed', organization=organization)
Host.objects.create(name='host1', inventory=constructed)
assert Host.objects.active_count() == 1
def test_org_active_count_minus_constructed(self, organization):
"""Org-scoped count must also exclude constructed-inventory shadow rows."""
inv = Inventory.objects.create(name='source-inv', organization=organization)
inv.hosts.create(name='host1')
assert Host.objects.org_active_count(organization.id) == 1
constructed = Inventory.objects.create(name='constructed-inv', kind='constructed', organization=organization)
Host.objects.create(name='host1', inventory=constructed)
assert Host.objects.org_active_count(organization.id) == 1
def test_host_case_insensitivity(self, organization):
inv1 = Inventory.objects.create(name='inv1', organization=organization)
inv2 = Inventory.objects.create(name='inv2', organization=organization)