diff --git a/awx/api/serializers.py b/awx/api/serializers.py index 63e6025211..cc19e3fbf7 100644 --- a/awx/api/serializers.py +++ b/awx/api/serializers.py @@ -1289,6 +1289,11 @@ class HostSerializer(BaseSerializerWithVariables): host, port = self._get_host_port_from_name(name) return value + def validate_inventory(self, value): + if value.kind == 'smart': + raise serializers.ValidationError({"detail": _("Cannot create Host for Smart Inventory")}) + return value + def validate(self, attrs): name = force_text(attrs.get('name', self.instance and self.instance.name or '')) host, port = self._get_host_port_from_name(name) @@ -1406,6 +1411,11 @@ class GroupSerializer(BaseSerializerWithVariables): if value in ('all', '_meta'): raise serializers.ValidationError(_('Invalid group name.')) return value + + def validate_inventory(self, value): + if value.kind == 'smart': + raise serializers.ValidationError({"detail": _("Cannot create Group for Smart Inventory")}) + return value def to_representation(self, obj): ret = super(GroupSerializer, self).to_representation(obj) @@ -1660,6 +1670,11 @@ class InventorySourceSerializer(UnifiedJobTemplateSerializer, InventorySourceOpt raise serializers.ValidationError(_("Setting not compatible with existing schedules.")) return value + def validate_inventory(self, value): + if value.kind == 'smart': + raise serializers.ValidationError({"detail": _("Cannot create Inventory Source for Smart Inventory")}) + return value + def validate(self, attrs): def get_field_from_model_or_attrs(fd): return attrs.get(fd, self.instance and getattr(self.instance, fd) or None) diff --git a/awx/api/templates/api/inventory_inventory_sources_update.md b/awx/api/templates/api/inventory_inventory_sources_update.md index a5862d16ad..f0f6d338d8 100644 --- a/awx/api/templates/api/inventory_inventory_sources_update.md +++ b/awx/api/templates/api/inventory_inventory_sources_update.md @@ -20,7 +20,8 @@ inventory sources: * `project_update`: ID of the project update job that was started if this inventory source is an SCM source. (interger, read-only, optional) -> *Note:* All manual inventory sources (source='') will be ignored by the update_inventory_sources endpoint. +Note: All manual inventory sources (source=' ') will be ignored by the update_inventory_sources endpoint. This endpoint will not update inventory sources for Smart Inventories. + Response code from this action will be: diff --git a/awx/api/views.py b/awx/api/views.py index e783b94d0b..046754b6a5 100644 --- a/awx/api/views.py +++ b/awx/api/views.py @@ -2480,6 +2480,8 @@ class InventoryInventorySourcesUpdate(RetrieveAPIView): def retrieve(self, request, *args, **kwargs): inventory = self.get_object() + if inventory.kind =='smart': + return Response(dict(error=_("Smart Inventories cannot host dynamic inventory sources.")), status=status.HTTP_400_BAD_REQUEST) update_data = [] for inventory_source in inventory.inventory_sources.exclude(source=''): details = {'inventory_source': inventory_source.pk, @@ -2492,6 +2494,8 @@ class InventoryInventorySourcesUpdate(RetrieveAPIView): update_data = [] successes = 0 failures = 0 + if inventory.kind =='smart': + return Response(dict(error=_("Action cannot be completed with Smart Inventory.")), status=status.HTTP_400_BAD_REQUEST) for inventory_source in inventory.inventory_sources.exclude(source=''): details = {'inventory_source': inventory_source.pk, 'status': None} can_update = inventory_source.can_update diff --git a/awx/main/tests/functional/api/test_inventory.py b/awx/main/tests/functional/api/test_inventory.py index 192c72ad86..2dee4e8631 100644 --- a/awx/main/tests/functional/api/test_inventory.py +++ b/awx/main/tests/functional/api/test_inventory.py @@ -5,7 +5,9 @@ from django.core.exceptions import ValidationError from awx.api.versioning import reverse -from awx.main.models import InventorySource +from awx.main.models import InventorySource, Inventory + +import json @pytest.fixture @@ -175,6 +177,54 @@ def test_delete_inventory_group(delete, group, alice, role_field, expected_statu delete(reverse('api:group_detail', kwargs={'pk': group.id}), alice, expect=expected_status_code) +@pytest.mark.django_db +def test_create_inventory_smarthost(post, get, inventory, admin_user, organization): + data = { 'name': 'Host 1', 'description': 'Test Host'} + smart_inventory = Inventory(name='smart', + kind='smart', + organization=organization, + host_filter='inventory_sources__source=ec2') + smart_inventory.save() + post(reverse('api:inventory_hosts_list', kwargs={'pk': smart_inventory.id}), data, admin_user) + resp = get(reverse('api:inventory_hosts_list', kwargs={'pk': smart_inventory.id}), admin_user) + jdata = json.loads(resp.content) + + assert getattr(smart_inventory, 'kind') == 'smart' + assert jdata['count'] == 0 + + +@pytest.mark.django_db +def test_create_inventory_smartgroup(post, get, inventory, admin_user, organization): + data = { 'name': 'Group 1', 'description': 'Test Group'} + smart_inventory = Inventory(name='smart', + kind='smart', + organization=organization, + host_filter='inventory_sources__source=ec2') + smart_inventory.save() + post(reverse('api:inventory_groups_list', kwargs={'pk': smart_inventory.id}), data, admin_user) + resp = get(reverse('api:inventory_groups_list', kwargs={'pk': smart_inventory.id}), admin_user) + jdata = json.loads(resp.content) + + assert getattr(smart_inventory, 'kind') == 'smart' + assert jdata['count'] == 0 + + +@pytest.mark.django_db +def test_create_inventory_smart_inventory_sources(post, get, inventory, admin_user, organization): + data = { 'name': 'Inventory Source 1', 'description': 'Test Inventory Source'} + smart_inventory = Inventory(name='smart', + kind='smart', + organization=organization, + host_filter='inventory_sources__source=ec2') + smart_inventory.save() + post(reverse('api:inventory_inventory_sources_list', kwargs={'pk': smart_inventory.id}), data, admin_user) + resp = get(reverse('api:inventory_inventory_sources_list', kwargs={'pk': smart_inventory.id}), admin_user) + jdata = json.loads(resp.content) + + assert getattr(smart_inventory, 'kind') == 'smart' + assert jdata['count'] == 0 + + @pytest.mark.parametrize("role_field,expected_status_code", [ (None, 403), ('admin_role', 201), diff --git a/awx/main/tests/unit/api/test_views.py b/awx/main/tests/unit/api/test_views.py index 6cc4f411d4..8a0a22aa48 100644 --- a/awx/main/tests/unit/api/test_views.py +++ b/awx/main/tests/unit/api/test_views.py @@ -116,8 +116,8 @@ class TestInventoryInventorySourcesUpdate: def exclude(self, **kwargs): return self.all() - Inventory = namedtuple('Inventory', ['inventory_sources']) - obj = Inventory(inventory_sources=InventorySources()) + Inventory = namedtuple('Inventory', ['inventory_sources', 'kind']) + obj = Inventory(inventory_sources=InventorySources(), kind='') mock_request = mocker.MagicMock() mock_request.user.can_access.return_value = can_access diff --git a/awx/playbooks/project_update.yml b/awx/playbooks/project_update.yml index e9b7ad19f7..20c84b6664 100644 --- a/awx/playbooks/project_update.yml +++ b/awx/playbooks/project_update.yml @@ -156,4 +156,4 @@ copy: dest: "{{ scm_revision_output }}" content: "{{ scm_version }}" - when: scm_version is defined and scm_revision_output is defined + when: scm_version is defined and scm_revision_output is defined \ No newline at end of file