mirror of
https://github.com/ansible/awx.git
synced 2026-03-19 01:47:31 -02:30
Merge pull request #13463 from AlanCoding/constructed_view
[constructed-inventory] Add views and serializers for special constructed inventory endpoints
This commit is contained in:
@@ -1715,6 +1715,7 @@ class InventorySerializer(LabelsListMixin, BaseSerializerWithVariables):
|
|||||||
res['organization'] = self.reverse('api:organization_detail', kwargs={'pk': obj.organization.pk})
|
res['organization'] = self.reverse('api:organization_detail', kwargs={'pk': obj.organization.pk})
|
||||||
if obj.kind == 'constructed':
|
if obj.kind == 'constructed':
|
||||||
res['source_inventories'] = self.reverse('api:inventory_source_inventories', kwargs={'pk': obj.pk})
|
res['source_inventories'] = self.reverse('api:inventory_source_inventories', kwargs={'pk': obj.pk})
|
||||||
|
res['url'] = self.reverse('api:constructed_inventory_detail', kwargs={'pk': obj.pk})
|
||||||
return res
|
return res
|
||||||
|
|
||||||
def to_representation(self, obj):
|
def to_representation(self, obj):
|
||||||
@@ -1756,6 +1757,81 @@ class InventorySerializer(LabelsListMixin, BaseSerializerWithVariables):
|
|||||||
return super(InventorySerializer, self).validate(attrs)
|
return super(InventorySerializer, self).validate(attrs)
|
||||||
|
|
||||||
|
|
||||||
|
class ConstructedFieldMixin(serializers.Field):
|
||||||
|
def get_attribute(self, instance):
|
||||||
|
if not hasattr(instance, '_constructed_inv_src'):
|
||||||
|
instance._constructed_inv_src = instance.inventory_sources.first()
|
||||||
|
inv_src = instance._constructed_inv_src
|
||||||
|
return super().get_attribute(inv_src) # yoink
|
||||||
|
|
||||||
|
|
||||||
|
class ConstructedCharField(ConstructedFieldMixin, serializers.CharField):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class ConstructedIntegerField(ConstructedFieldMixin, serializers.IntegerField):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class ConstructedInventorySerializer(InventorySerializer):
|
||||||
|
source_vars = ConstructedCharField(
|
||||||
|
required=False,
|
||||||
|
default=None,
|
||||||
|
allow_blank=True,
|
||||||
|
help_text=_('The source_vars for the related auto-created inventory source, special to constructed inventory.'),
|
||||||
|
)
|
||||||
|
update_cache_timeout = ConstructedIntegerField(
|
||||||
|
required=False,
|
||||||
|
allow_null=True,
|
||||||
|
min_value=0,
|
||||||
|
default=None,
|
||||||
|
help_text=_('The cache timeout for the related auto-created inventory source, special to constructed inventory'),
|
||||||
|
)
|
||||||
|
limit = ConstructedCharField(
|
||||||
|
required=False,
|
||||||
|
default=None,
|
||||||
|
allow_blank=True,
|
||||||
|
help_text=_('The limit to restrict the returned hosts for the related auto-created inventory source, special to constructed inventory.'),
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Inventory
|
||||||
|
fields = ('*', '-host_filter', 'source_vars', 'update_cache_timeout', 'limit')
|
||||||
|
|
||||||
|
def pop_inv_src_data(self, data):
|
||||||
|
inv_src_data = {}
|
||||||
|
for field in ('source_vars', 'update_cache_timeout', 'limit'):
|
||||||
|
if field in data:
|
||||||
|
# values always need to be removed, as they are not valid for Inventory model
|
||||||
|
value = data.pop(field)
|
||||||
|
# null is not valid for any of those fields, taken as not-provided
|
||||||
|
if value is not None:
|
||||||
|
inv_src_data[field] = value
|
||||||
|
return inv_src_data
|
||||||
|
|
||||||
|
def apply_inv_src_data(self, inventory, inv_src_data):
|
||||||
|
if inv_src_data:
|
||||||
|
update_fields = []
|
||||||
|
inv_src = inventory.inventory_sources.first()
|
||||||
|
for field, value in inv_src_data.items():
|
||||||
|
setattr(inv_src, field, value)
|
||||||
|
update_fields.append(field)
|
||||||
|
if update_fields:
|
||||||
|
inv_src.save(update_fields=update_fields)
|
||||||
|
|
||||||
|
def create(self, validated_data):
|
||||||
|
inv_src_data = self.pop_inv_src_data(validated_data)
|
||||||
|
inventory = super().create(validated_data)
|
||||||
|
self.apply_inv_src_data(inventory, inv_src_data)
|
||||||
|
return inventory
|
||||||
|
|
||||||
|
def update(self, obj, validated_data):
|
||||||
|
inv_src_data = self.pop_inv_src_data(validated_data)
|
||||||
|
obj = super().update(obj, validated_data)
|
||||||
|
self.apply_inv_src_data(obj, inv_src_data)
|
||||||
|
return obj
|
||||||
|
|
||||||
|
|
||||||
class InventoryScriptSerializer(InventorySerializer):
|
class InventoryScriptSerializer(InventorySerializer):
|
||||||
class Meta:
|
class Meta:
|
||||||
fields = ()
|
fields = ()
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ from django.urls import re_path
|
|||||||
from awx.api.views.inventory import (
|
from awx.api.views.inventory import (
|
||||||
InventoryList,
|
InventoryList,
|
||||||
InventoryDetail,
|
InventoryDetail,
|
||||||
|
ConstructedInventoryDetail,
|
||||||
|
ConstructedInventoryList,
|
||||||
InventoryActivityStreamList,
|
InventoryActivityStreamList,
|
||||||
InventorySourceInventoriesList,
|
InventorySourceInventoriesList,
|
||||||
InventoryJobTemplateList,
|
InventoryJobTemplateList,
|
||||||
@@ -50,4 +52,10 @@ urls = [
|
|||||||
re_path(r'^(?P<pk>[0-9]+)/copy/$', InventoryCopy.as_view(), name='inventory_copy'),
|
re_path(r'^(?P<pk>[0-9]+)/copy/$', InventoryCopy.as_view(), name='inventory_copy'),
|
||||||
]
|
]
|
||||||
|
|
||||||
__all__ = ['urls']
|
# Constructed inventory special views
|
||||||
|
constructed_inventory_urls = [
|
||||||
|
re_path(r'^$', ConstructedInventoryList.as_view(), name='constructed_inventory_list'),
|
||||||
|
re_path(r'^(?P<pk>[0-9]+)/$', ConstructedInventoryDetail.as_view(), name='constructed_inventory_detail'),
|
||||||
|
]
|
||||||
|
|
||||||
|
__all__ = ['urls', 'constructed_inventory_urls']
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ from .organization import urls as organization_urls
|
|||||||
from .user import urls as user_urls
|
from .user import urls as user_urls
|
||||||
from .project import urls as project_urls
|
from .project import urls as project_urls
|
||||||
from .project_update import urls as project_update_urls
|
from .project_update import urls as project_update_urls
|
||||||
from .inventory import urls as inventory_urls
|
from .inventory import urls as inventory_urls, constructed_inventory_urls
|
||||||
from .execution_environments import urls as execution_environment_urls
|
from .execution_environments import urls as execution_environment_urls
|
||||||
from .team import urls as team_urls
|
from .team import urls as team_urls
|
||||||
from .host import urls as host_urls
|
from .host import urls as host_urls
|
||||||
@@ -110,6 +110,7 @@ v2_urls = [
|
|||||||
re_path(r'^project_updates/', include(project_update_urls)),
|
re_path(r'^project_updates/', include(project_update_urls)),
|
||||||
re_path(r'^teams/', include(team_urls)),
|
re_path(r'^teams/', include(team_urls)),
|
||||||
re_path(r'^inventories/', include(inventory_urls)),
|
re_path(r'^inventories/', include(inventory_urls)),
|
||||||
|
re_path(r'^constructed_inventories/', include(constructed_inventory_urls)),
|
||||||
re_path(r'^hosts/', include(host_urls)),
|
re_path(r'^hosts/', include(host_urls)),
|
||||||
re_path(r'^groups/', include(group_urls)),
|
re_path(r'^groups/', include(group_urls)),
|
||||||
re_path(r'^inventory_sources/', include(inventory_source_urls)),
|
re_path(r'^inventory_sources/', include(inventory_source_urls)),
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ from awx.api.views.labels import LabelSubListCreateAttachDetachView
|
|||||||
|
|
||||||
from awx.api.serializers import (
|
from awx.api.serializers import (
|
||||||
InventorySerializer,
|
InventorySerializer,
|
||||||
|
ConstructedInventorySerializer,
|
||||||
ActivityStreamSerializer,
|
ActivityStreamSerializer,
|
||||||
RoleSerializer,
|
RoleSerializer,
|
||||||
InstanceGroupSerializer,
|
InstanceGroupSerializer,
|
||||||
@@ -82,7 +83,9 @@ class InventoryDetail(RelatedJobsPreventDeleteMixin, RetrieveUpdateDestroyAPIVie
|
|||||||
|
|
||||||
# Do not allow changes to an Inventory kind.
|
# Do not allow changes to an Inventory kind.
|
||||||
if kind is not None and obj.kind != kind:
|
if kind is not None and obj.kind != kind:
|
||||||
return Response(dict(error=_('You cannot turn a regular inventory into a "smart" inventory.')), status=status.HTTP_405_METHOD_NOT_ALLOWED)
|
return Response(
|
||||||
|
dict(error=_('You cannot turn a regular inventory into a "smart" or "constructed" inventory.')), status=status.HTTP_405_METHOD_NOT_ALLOWED
|
||||||
|
)
|
||||||
return super(InventoryDetail, self).update(request, *args, **kwargs)
|
return super(InventoryDetail, self).update(request, *args, **kwargs)
|
||||||
|
|
||||||
def destroy(self, request, *args, **kwargs):
|
def destroy(self, request, *args, **kwargs):
|
||||||
@@ -97,6 +100,20 @@ class InventoryDetail(RelatedJobsPreventDeleteMixin, RetrieveUpdateDestroyAPIVie
|
|||||||
return Response(dict(error=_("{0}".format(e))), status=status.HTTP_400_BAD_REQUEST)
|
return Response(dict(error=_("{0}".format(e))), status=status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
|
||||||
|
class ConstructedInventoryDetail(InventoryDetail):
|
||||||
|
|
||||||
|
serializer_class = ConstructedInventorySerializer
|
||||||
|
|
||||||
|
|
||||||
|
class ConstructedInventoryList(InventoryList):
|
||||||
|
|
||||||
|
serializer_class = ConstructedInventorySerializer
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
r = super().get_queryset()
|
||||||
|
return r.filter(kind='constructed')
|
||||||
|
|
||||||
|
|
||||||
class InventorySourceInventoriesList(SubListAttachDetachAPIView):
|
class InventorySourceInventoriesList(SubListAttachDetachAPIView):
|
||||||
model = Inventory
|
model = Inventory
|
||||||
serializer_class = InventorySerializer
|
serializer_class = InventorySerializer
|
||||||
|
|||||||
@@ -101,6 +101,7 @@ class ApiVersionRootView(APIView):
|
|||||||
data['tokens'] = reverse('api:o_auth2_token_list', request=request)
|
data['tokens'] = reverse('api:o_auth2_token_list', request=request)
|
||||||
data['metrics'] = reverse('api:metrics_view', request=request)
|
data['metrics'] = reverse('api:metrics_view', request=request)
|
||||||
data['inventory'] = reverse('api:inventory_list', request=request)
|
data['inventory'] = reverse('api:inventory_list', request=request)
|
||||||
|
data['constructed_inventory'] = reverse('api:constructed_inventory_list', request=request)
|
||||||
data['inventory_sources'] = reverse('api:inventory_source_list', request=request)
|
data['inventory_sources'] = reverse('api:inventory_source_list', request=request)
|
||||||
data['inventory_updates'] = reverse('api:inventory_update_list', request=request)
|
data['inventory_updates'] = reverse('api:inventory_update_list', request=request)
|
||||||
data['groups'] = reverse('api:group_list', request=request)
|
data['groups'] = reverse('api:group_list', request=request)
|
||||||
|
|||||||
@@ -594,3 +594,45 @@ class TestControlledBySCM:
|
|||||||
rando,
|
rando,
|
||||||
expect=403,
|
expect=403,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
class TestConstructedInventory:
|
||||||
|
@pytest.fixture
|
||||||
|
def constructed_inventory(self, organization):
|
||||||
|
return Inventory.objects.create(name='constructed-test-inventory', kind='constructed', organization=organization)
|
||||||
|
|
||||||
|
def test_get_constructed_inventory(self, constructed_inventory, admin_user, get):
|
||||||
|
inv_src = constructed_inventory.inventory_sources.first()
|
||||||
|
inv_src.update_cache_timeout = 53
|
||||||
|
inv_src.save(update_fields=['update_cache_timeout'])
|
||||||
|
r = get(url=reverse('api:constructed_inventory_detail', kwargs={'pk': constructed_inventory.pk}), user=admin_user, expect=200)
|
||||||
|
assert r.data['update_cache_timeout'] == 53
|
||||||
|
|
||||||
|
def test_patch_constructed_inventory(self, constructed_inventory, admin_user, patch):
|
||||||
|
inv_src = constructed_inventory.inventory_sources.first()
|
||||||
|
assert inv_src.update_cache_timeout == 0
|
||||||
|
assert inv_src.limit == ''
|
||||||
|
r = patch(
|
||||||
|
url=reverse('api:constructed_inventory_detail', kwargs={'pk': constructed_inventory.pk}),
|
||||||
|
data=dict(update_cache_timeout=54, limit='foobar'),
|
||||||
|
user=admin_user,
|
||||||
|
expect=200,
|
||||||
|
)
|
||||||
|
assert r.data['update_cache_timeout'] == 54
|
||||||
|
inv_src = constructed_inventory.inventory_sources.first()
|
||||||
|
assert inv_src.update_cache_timeout == 54
|
||||||
|
assert inv_src.limit == 'foobar'
|
||||||
|
|
||||||
|
def test_create_constructed_inventory(self, constructed_inventory, admin_user, post, organization):
|
||||||
|
r = post(
|
||||||
|
url=reverse('api:constructed_inventory_list'),
|
||||||
|
data=dict(name='constructed-inventory-just-created', kind='constructed', organization=organization.id, update_cache_timeout=55, limit='foobar'),
|
||||||
|
user=admin_user,
|
||||||
|
expect=201,
|
||||||
|
)
|
||||||
|
pk = r.data['id']
|
||||||
|
constructed_inventory = Inventory.objects.get(pk=pk)
|
||||||
|
inv_src = constructed_inventory.inventory_sources.first()
|
||||||
|
assert inv_src.update_cache_timeout == 55
|
||||||
|
assert inv_src.limit == 'foobar'
|
||||||
|
|||||||
Reference in New Issue
Block a user