Keep inventory sources in their inventory's organization

An inventory source copies its inventory's organization only when it is
created, and the API can't change it afterwards, so when an inventory
moves to another organization its sources stay behind. Inventory updates
copy the source's organization, so they are filed under the old
organization too, and their execution environment can fall back to the
old organization's default.

Have Inventory.save bring its sources' organization in line whenever it
is saved with organization, which also repairs sources that are already
stale the next time their inventory is saved.

related #16702

Assisted-by: Claude Opus 5.5
Signed-off-by: Quanah Gibson-Mount <quanah@dropbox.com>
This commit is contained in:
Quanah Gibson-Mount
2026-10-13 14:45:01 -04:00
committed by akus062381
parent 5613b5ac6c
commit a17a4bc15f
2 changed files with 43 additions and 1 deletions
+5
View File
@@ -481,6 +481,11 @@ class Inventory(CommonModelNameNotUnique, RelatedJobsMixin, OpaQueryPathMixin):
def save(self, *args, **kwargs):
self._update_host_smart_inventory_memeberships()
super(Inventory, self).save(*args, **kwargs)
update_fields = kwargs.get('update_fields')
if update_fields is None or {'organization', 'organization_id'} & set(update_fields):
# An inventory source takes its organization from its inventory and can't set its own, so keep it in step
# when the inventory moves.
self.inventory_sources.exclude(organization_id=self.organization_id).update(organization_id=self.organization_id)
if self.kind == 'smart' and 'host_filter' in kwargs.get('update_fields', ['host_filter']) and connection.vendor != 'sqlite':
# Minimal update of host_count for smart inventory host filter changes
self.update_computed_fields()
@@ -4,7 +4,7 @@ import pytest
from unittest import mock
# AWX
from awx.main.models import Host, Inventory, InventorySource, InventoryUpdate, CredentialType, Credential, Job
from awx.main.models import Host, Inventory, InventorySource, InventoryUpdate, CredentialType, Credential, Job, Organization
from awx.main.utils.filters import SmartFilter
from awx.main.utils.plugins import discover_available_cloud_provider_plugin_names
@@ -385,3 +385,40 @@ class TestHostManager:
# 2 organizations with host of same name only has 1 entry in smart inventory
# smart inventory in 1 organization does not include host from another
# smart inventory correctly returns hosts in filter in same organization
@pytest.mark.django_db
class TestInventorySourceOrganization:
"""An inventory source can't set its organization; it follows its inventory's."""
def test_new_source_takes_inventory_organization(self, inventory):
source = inventory.inventory_sources.create(name='source', source='ec2')
assert source.organization_id == inventory.organization_id
def test_moving_inventory_moves_its_sources(self, inventory, inventory_source):
other = Organization.objects.create(name='other-org')
inventory.organization = other
inventory.save()
inventory_source.refresh_from_db()
assert inventory_source.organization_id == other.id
def test_inventory_updates_carry_the_new_organization(self, inventory, inventory_source):
other = Organization.objects.create(name='other-org')
inventory.organization = other
inventory.save()
inventory_source.refresh_from_db()
assert inventory_source.create_unified_job().organization_id == other.id
def test_save_repairs_a_source_left_behind(self, inventory, inventory_source):
other = Organization.objects.create(name='other-org')
InventorySource.objects.filter(pk=inventory_source.pk).update(organization=other)
inventory.save()
inventory_source.refresh_from_db()
assert inventory_source.organization_id == inventory.organization_id
def test_partial_save_without_organization_leaves_sources_alone(self, inventory, inventory_source):
other = Organization.objects.create(name='other-org')
InventorySource.objects.filter(pk=inventory_source.pk).update(organization=other)
inventory.save(update_fields=['description'])
inventory_source.refresh_from_db()
assert inventory_source.organization_id == other.id