diff --git a/awx/main/models/inventory.py b/awx/main/models/inventory.py index 4157d96921..ca34aab684 100644 --- a/awx/main/models/inventory.py +++ b/awx/main/models/inventory.py @@ -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() diff --git a/awx/main/tests/functional/models/test_inventory.py b/awx/main/tests/functional/models/test_inventory.py index 8811b92258..404f1bb99b 100644 --- a/awx/main/tests/functional/models/test_inventory.py +++ b/awx/main/tests/functional/models/test_inventory.py @@ -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