diff --git a/awx/api/generics.py b/awx/api/generics.py index ad14074852..d1d168fc56 100644 --- a/awx/api/generics.py +++ b/awx/api/generics.py @@ -1023,6 +1023,9 @@ class GenericCancelView(RetrieveAPIView): # In subclass set model, serializer_class obj_permission_type = 'cancel' + def get(self, request, *args, **kwargs): + return super(GenericCancelView, self).get(request, *args, **kwargs) + @transaction.non_atomic_requests def dispatch(self, *args, **kwargs): return super(GenericCancelView, self).dispatch(*args, **kwargs) diff --git a/awx/api/views/__init__.py b/awx/api/views/__init__.py index 772b1306b5..7ea220e5d3 100644 --- a/awx/api/views/__init__.py +++ b/awx/api/views/__init__.py @@ -56,6 +56,7 @@ from wsgiref.util import FileWrapper # django-ansible-base from ansible_base.lib.utils.requests import get_remote_hosts from ansible_base.rbac.models import RoleEvaluation +from ansible_base.lib.utils.schema import extend_schema_if_available # AWX from awx.main.tasks.system import send_notifications, update_inventory_computed_fields @@ -165,7 +166,9 @@ class DashboardView(APIView): name = _("Dashboard") swagger_topic = 'Dashboard' + resource_purpose = 'dashboard aggregate statistics' + @extend_schema_if_available(extensions={"x-ai-description": "Get aggregate statistics for Tower"}) def get(self, request, format=None): '''Show Dashboard Details''' data = OrderedDict() @@ -261,7 +264,9 @@ class DashboardView(APIView): class DashboardJobsGraphView(APIView): name = _("Dashboard Jobs Graphs") swagger_topic = 'Jobs' + resource_purpose = 'dashboard jobs graph data' + @extend_schema_if_available(extensions={"x-ai-description": "Get dasboard data for jobs"}) def get(self, request, format=None): period = request.query_params.get('period', 'month') job_type = request.query_params.get('job_type', 'all') @@ -362,6 +367,7 @@ class InstanceList(ListCreateAPIView): serializer_class = serializers.InstanceSerializer search_fields = ('hostname',) ordering = ('id',) + resource_purpose = 'instances' def get_queryset(self): qs = super().get_queryset().prefetch_related('receptor_addresses') @@ -372,6 +378,7 @@ class InstanceDetail(RetrieveUpdateAPIView): name = _("Instance Detail") model = models.Instance serializer_class = serializers.InstanceSerializer + resource_purpose = 'instance detail' def get_queryset(self): qs = super().get_queryset().prefetch_related('receptor_addresses') @@ -400,6 +407,7 @@ class InstanceUnifiedJobsList(SubListAPIView): model = models.UnifiedJob serializer_class = serializers.UnifiedJobListSerializer parent_model = models.Instance + resource_purpose = 'jobs executed on an instance' def get_queryset(self): po = self.get_parent_object() @@ -416,8 +424,10 @@ class InstancePeersList(SubListAPIView): parent_access = 'read' relationship = 'peers' search_fields = ('address',) + resource_purpose = 'peers of an instance' +@extend_schema_if_available(extensions={"x-ai-description": "List receptor addresses of instance group"}) class InstanceReceptorAddressesList(SubListAPIView): name = _("Receptor Addresses") model = models.ReceptorAddress @@ -425,6 +435,7 @@ class InstanceReceptorAddressesList(SubListAPIView): parent_model = models.Instance serializer_class = serializers.ReceptorAddressSerializer search_fields = ('address',) + resource_purpose = 'receptor addresses of an instance' class ReceptorAddressesList(ListAPIView): @@ -432,6 +443,7 @@ class ReceptorAddressesList(ListAPIView): model = models.ReceptorAddress serializer_class = serializers.ReceptorAddressSerializer search_fields = ('address',) + resource_purpose = 'receptor addresses' class ReceptorAddressDetail(RetrieveAPIView): @@ -440,6 +452,7 @@ class ReceptorAddressDetail(RetrieveAPIView): serializer_class = serializers.ReceptorAddressSerializer parent_model = models.Instance relationship = 'receptor_addresses' + resource_purpose = 'receptor address detail' class InstanceInstanceGroupsList(InstanceGroupMembershipMixin, SubListCreateAttachDetachAPIView): @@ -448,6 +461,7 @@ class InstanceInstanceGroupsList(InstanceGroupMembershipMixin, SubListCreateAtta serializer_class = serializers.InstanceGroupSerializer parent_model = models.Instance relationship = 'rampart_groups' + resource_purpose = 'instance groups of an instance' def is_valid_relation(self, parent, sub, created=False): if parent.node_type == 'control': @@ -470,16 +484,19 @@ class InstanceHealthCheck(GenericAPIView): model = models.Instance serializer_class = serializers.InstanceHealthCheckSerializer permission_classes = (IsSystemAdminOrAuditor,) + resource_purpose = 'instance health check' def get_queryset(self): return super().get_queryset().filter(node_type='execution') # FIXME: For now, we don't have a good way of checking the health of a hop node. + @extend_schema_if_available(extensions={"x-ai-description": "Get instance health check result"}) def get(self, request, *args, **kwargs): obj = self.get_object() data = self.get_serializer(data=request.data).to_representation(obj) return Response(data, status=status.HTTP_200_OK) + @extend_schema_if_available(extensions={"x-ai-description": "Perform instance health check"}) def post(self, request, *args, **kwargs): obj = self.get_object() if obj.health_check_pending: @@ -504,6 +521,7 @@ class InstanceGroupList(ListCreateAPIView): name = _("Instance Groups") model = models.InstanceGroup serializer_class = serializers.InstanceGroupSerializer + resource_purpose = 'instance groups' class InstanceGroupDetail(RelatedJobsPreventDeleteMixin, RetrieveUpdateDestroyAPIView): @@ -511,6 +529,7 @@ class InstanceGroupDetail(RelatedJobsPreventDeleteMixin, RetrieveUpdateDestroyAP name = _("Instance Group Detail") model = models.InstanceGroup serializer_class = serializers.InstanceGroupSerializer + resource_purpose = 'instance group detail' def update_raw_data(self, data): if self.get_object().is_container_group: @@ -526,11 +545,14 @@ class InstanceGroupUnifiedJobsList(SubListAPIView): serializer_class = serializers.UnifiedJobListSerializer parent_model = models.InstanceGroup relationship = "unifiedjob_set" + resource_purpose = 'jobs of an instance group' +@extend_schema_if_available(extensions={"x-ai-description": "Retrieve access list of an instance group"}) class InstanceGroupAccessList(ResourceAccessList): model = models.User # needs to be User for AccessLists parent_model = models.InstanceGroup + resource_purpose = 'users who can access the instance group' class InstanceGroupObjectRolesList(SubListAPIView): @@ -539,6 +561,7 @@ class InstanceGroupObjectRolesList(SubListAPIView): serializer_class = serializers.RoleSerializer parent_model = models.InstanceGroup search_fields = ('role_field', 'content_type__model') + resource_purpose = 'roles of an instance group' def get_queryset(self): po = self.get_parent_object() @@ -553,6 +576,7 @@ class InstanceGroupInstanceList(InstanceGroupMembershipMixin, SubListAttachDetac parent_model = models.InstanceGroup relationship = "instances" search_fields = ('hostname',) + resource_purpose = 'instance of an instance group' def is_valid_relation(self, parent, sub, created=False): if sub.node_type == 'control': @@ -575,11 +599,13 @@ class ScheduleList(ListCreateAPIView): model = models.Schedule serializer_class = serializers.ScheduleSerializer ordering = ('id',) + resource_purpose = 'schedules' class ScheduleDetail(RetrieveUpdateDestroyAPIView): model = models.Schedule serializer_class = serializers.ScheduleSerializer + resource_purpose = 'schedule detail' class SchedulePreview(GenericAPIView): @@ -587,7 +613,9 @@ class SchedulePreview(GenericAPIView): name = _('Schedule Recurrence Rule Preview') serializer_class = serializers.SchedulePreviewSerializer permission_classes = (IsAuthenticated,) + resource_purpose = 'schedule recurrence rule preview' + @extend_schema_if_available(extensions={"x-ai-description": "Preview schedule recurrence rule occurrences"}) def post(self, request): serializer = self.get_serializer(data=request.data) if serializer.is_valid(): @@ -610,7 +638,9 @@ class SchedulePreview(GenericAPIView): class ScheduleZoneInfo(APIView): swagger_topic = 'System Configuration' + resource_purpose = 'timezone information for schedules' + @extend_schema_if_available(extensions={"x-ai-description": "Get timezone information for schedules"}) def get(self, request): return Response({'zones': models.Schedule.get_zoneinfo(), 'links': models.Schedule.get_zoneinfo_links()}) @@ -619,6 +649,7 @@ class LaunchConfigCredentialsBase(SubListAttachDetachAPIView): model = models.Credential serializer_class = serializers.CredentialSerializer relationship = 'credentials' + resource_purpose = 'credentials of a launch configuration' def is_valid_relation(self, parent, sub, created=False): if not parent.unified_job_template: @@ -648,10 +679,12 @@ class LaunchConfigCredentialsBase(SubListAttachDetachAPIView): class ScheduleCredentialsList(LaunchConfigCredentialsBase): parent_model = models.Schedule + resource_purpose = 'credentials of a schedule' class ScheduleLabelsList(LabelSubListCreateAttachDetachView): parent_model = models.Schedule + resource_purpose = 'labels of a schedule' class ScheduleInstanceGroupList(SubListAttachDetachAPIView): @@ -659,6 +692,7 @@ class ScheduleInstanceGroupList(SubListAttachDetachAPIView): serializer_class = serializers.InstanceGroupSerializer parent_model = models.Schedule relationship = 'instance_groups' + resource_purpose = 'instance groups of a schedule' class ScheduleUnifiedJobsList(SubListAPIView): @@ -667,16 +701,19 @@ class ScheduleUnifiedJobsList(SubListAPIView): parent_model = models.Schedule relationship = 'unifiedjob_set' name = _('Schedule Jobs List') + resource_purpose = 'jobs created by a schedule' class TeamList(ListCreateAPIView): model = models.Team serializer_class = serializers.TeamSerializer + resource_purpose = 'teams' class TeamDetail(RetrieveUpdateDestroyAPIView): model = models.Team serializer_class = serializers.TeamSerializer + resource_purpose = 'team detail' class TeamUsersList(BaseUsersList): @@ -685,6 +722,7 @@ class TeamUsersList(BaseUsersList): parent_model = models.Team relationship = 'member_role.members' ordering = ('username',) + resource_purpose = 'users of a team' class TeamRolesList(SubListAttachDetachAPIView): @@ -693,6 +731,7 @@ class TeamRolesList(SubListAttachDetachAPIView): serializer_class = serializers.RoleSerializerWithParentAccess metadata_class = RoleMetadata parent_model = models.Team + resource_purpose = 'roles of a team' relationship = 'member_role.children' search_fields = ('role_field', 'content_type__model') @@ -702,6 +741,7 @@ class TeamRolesList(SubListAttachDetachAPIView): raise PermissionDenied() return models.Role.filter_visible_roles(self.request.user, team.member_role.children.all().exclude(pk=team.read_role.pk)) + @extend_schema_if_available(extensions={"x-ai-description": "Add a role to a team"}) def post(self, request, *args, **kwargs): sub_id = request.data.get('id', None) if not sub_id: @@ -744,6 +784,7 @@ class TeamObjectRolesList(SubListAPIView): parent_model = models.Team search_fields = ('role_field', 'content_type__model') deprecated = True + resource_purpose = 'object roles of a team' def get_queryset(self): po = self.get_parent_object() @@ -755,6 +796,7 @@ class TeamProjectsList(SubListAPIView): model = models.Project serializer_class = serializers.ProjectSerializer parent_model = models.Team + resource_purpose = 'projects accessible to a team' def get_queryset(self): team = self.get_parent_object() @@ -770,6 +812,7 @@ class TeamActivityStreamList(SubListAPIView): parent_model = models.Team relationship = 'activitystream_set' search_fields = ('changes',) + resource_purpose = 'activity stream for a team' def get_queryset(self): parent = self.get_parent_object() @@ -799,6 +842,7 @@ class TeamActivityStreamList(SubListAPIView): class TeamAccessList(ResourceAccessList): model = models.User # needs to be User for AccessLists's parent_model = models.Team + resource_purpose = 'users who can access the team' class ExecutionEnvironmentList(ListCreateAPIView): @@ -806,6 +850,7 @@ class ExecutionEnvironmentList(ListCreateAPIView): model = models.ExecutionEnvironment serializer_class = serializers.ExecutionEnvironmentSerializer swagger_topic = "Execution Environments" + resource_purpose = 'execution environments' class ExecutionEnvironmentDetail(RetrieveUpdateDestroyAPIView): @@ -813,6 +858,7 @@ class ExecutionEnvironmentDetail(RetrieveUpdateDestroyAPIView): model = models.ExecutionEnvironment serializer_class = serializers.ExecutionEnvironmentSerializer swagger_topic = "Execution Environments" + resource_purpose = 'execution environment detail' def update(self, request, *args, **kwargs): instance = self.get_object() @@ -835,11 +881,13 @@ class ExecutionEnvironmentJobTemplateList(SubListAPIView): serializer_class = serializers.UnifiedJobTemplateSerializer parent_model = models.ExecutionEnvironment relationship = 'unifiedjobtemplates' + resource_purpose = 'unified job templates using this execution environment' class ExecutionEnvironmentCopy(CopyAPIView): model = models.ExecutionEnvironment copy_return_serializer_class = serializers.ExecutionEnvironmentSerializer + resource_purpose = 'copy of an existing execution environment' class ExecutionEnvironmentActivityStreamList(SubListAPIView): @@ -849,24 +897,29 @@ class ExecutionEnvironmentActivityStreamList(SubListAPIView): relationship = 'activitystream_set' search_fields = ('changes',) filter_read_permission = False + resource_purpose = 'activity stream of an execution environment' class ProjectList(ListCreateAPIView): model = models.Project serializer_class = serializers.ProjectSerializer + resource_purpose = 'projects' class ProjectDetail(RelatedJobsPreventDeleteMixin, RetrieveUpdateDestroyAPIView): model = models.Project serializer_class = serializers.ProjectSerializer + resource_purpose = 'project detail' class ProjectPlaybooks(RetrieveAPIView): model = models.Project serializer_class = serializers.ProjectPlaybooksSerializer + resource_purpose = 'playbooks of a project' class ProjectInventories(RetrieveAPIView): + resource_purpose = 'inventories from a project' model = models.Project serializer_class = serializers.ProjectInventoriesSerializer @@ -874,6 +927,7 @@ class ProjectInventories(RetrieveAPIView): class ProjectTeamsList(ListAPIView): model = models.Team serializer_class = serializers.TeamSerializer + resource_purpose = 'teams with access to a project' def get_queryset(self): parent = get_object_or_404(models.Project, pk=self.kwargs['pk']) @@ -903,6 +957,7 @@ class ProjectSchedulesList(SubListCreateAPIView): parent_model = models.Project relationship = 'schedules' parent_key = 'unified_job_template' + resource_purpose = 'schedules of a project' class ProjectScmInventorySources(SubListAPIView): @@ -912,6 +967,7 @@ class ProjectScmInventorySources(SubListAPIView): parent_model = models.Project relationship = 'scm_inventory_sources' parent_key = 'source_project' + resource_purpose = 'scm inventory sources of a project' class ProjectActivityStreamList(SubListAPIView): @@ -920,6 +976,7 @@ class ProjectActivityStreamList(SubListAPIView): parent_model = models.Project relationship = 'activitystream_set' search_fields = ('changes',) + resource_purpose = 'activity stream for a project' def get_queryset(self): parent = self.get_parent_object() @@ -936,18 +993,22 @@ class ProjectNotificationTemplatesAnyList(SubListCreateAttachDetachAPIView): model = models.NotificationTemplate serializer_class = serializers.NotificationTemplateSerializer parent_model = models.Project + resource_purpose = 'base view for notification templates of a project' class ProjectNotificationTemplatesStartedList(ProjectNotificationTemplatesAnyList): relationship = 'notification_templates_started' + resource_purpose = 'notification templates for project started events' class ProjectNotificationTemplatesErrorList(ProjectNotificationTemplatesAnyList): relationship = 'notification_templates_error' + resource_purpose = 'notification templates for project error events' class ProjectNotificationTemplatesSuccessList(ProjectNotificationTemplatesAnyList): relationship = 'notification_templates_success' + resource_purpose = 'notification templates for project success events' class ProjectUpdatesList(SubListAPIView): @@ -955,13 +1016,16 @@ class ProjectUpdatesList(SubListAPIView): serializer_class = serializers.ProjectUpdateListSerializer parent_model = models.Project relationship = 'project_updates' + resource_purpose = 'project updates of a project' class ProjectUpdateView(RetrieveAPIView): model = models.Project serializer_class = serializers.ProjectUpdateViewSerializer permission_classes = (ProjectUpdatePermission,) + resource_purpose = 'trigger project update' + @extend_schema_if_available(extensions={"x-ai-description": "Trigger a project update"}) def post(self, request, *args, **kwargs): obj = self.get_object() if obj.can_update: @@ -981,11 +1045,13 @@ class ProjectUpdateView(RetrieveAPIView): class ProjectUpdateList(ListAPIView): model = models.ProjectUpdate serializer_class = serializers.ProjectUpdateListSerializer + resource_purpose = 'project updates' class ProjectUpdateDetail(UnifiedJobDeletionMixin, RetrieveDestroyAPIView): model = models.ProjectUpdate serializer_class = serializers.ProjectUpdateDetailSerializer + resource_purpose = 'project update detail' class ProjectUpdateEventsList(SubListAPIView): @@ -996,6 +1062,7 @@ class ProjectUpdateEventsList(SubListAPIView): name = _('Project Update Events List') search_fields = ('stdout',) pagination_class = UnifiedJobEventPagination + resource_purpose = 'events of a project update' def finalize_response(self, request, response, *args, **kwargs): response['X-UI-Max-Events'] = settings.MAX_UI_JOB_EVENTS @@ -1015,6 +1082,7 @@ class SystemJobEventsList(SubListAPIView): name = _('System Job Events List') search_fields = ('stdout',) pagination_class = UnifiedJobEventPagination + resource_purpose = 'events of a system job' def finalize_response(self, request, response, *args, **kwargs): response['X-UI-Max-Events'] = settings.MAX_UI_JOB_EVENTS @@ -1029,6 +1097,7 @@ class SystemJobEventsList(SubListAPIView): class ProjectUpdateCancel(GenericCancelView): model = models.ProjectUpdate serializer_class = serializers.ProjectUpdateCancelSerializer + resource_purpose = 'cancel for a project update' class ProjectUpdateNotificationsList(SubListAPIView): @@ -1037,6 +1106,7 @@ class ProjectUpdateNotificationsList(SubListAPIView): parent_model = models.ProjectUpdate relationship = 'notifications' search_fields = ('subject', 'notification_type', 'body') + resource_purpose = 'notifications of a project update' class ProjectUpdateScmInventoryUpdates(SubListAPIView): @@ -1046,11 +1116,13 @@ class ProjectUpdateScmInventoryUpdates(SubListAPIView): parent_model = models.ProjectUpdate relationship = 'scm_inventory_updates' parent_key = 'source_project_update' + resource_purpose = 'scm inventory updates triggered by a project update' class ProjectAccessList(ResourceAccessList): model = models.User # needs to be User for AccessLists's parent_model = models.Project + resource_purpose = 'users who can access the project' class ProjectObjectRolesList(SubListAPIView): @@ -1060,6 +1132,7 @@ class ProjectObjectRolesList(SubListAPIView): parent_model = models.Project search_fields = ('role_field', 'content_type__model') deprecated = True + resource_purpose = 'roles of a project' def get_queryset(self): po = self.get_parent_object() @@ -1070,6 +1143,7 @@ class ProjectObjectRolesList(SubListAPIView): class ProjectCopy(CopyAPIView): model = models.Project copy_return_serializer_class = serializers.ProjectSerializer + resource_purpose = 'copy of a project' class UserList(ListCreateAPIView): @@ -1077,6 +1151,7 @@ class UserList(ListCreateAPIView): serializer_class = serializers.UserSerializer permission_classes = (UserPermission,) ordering = ('username',) + resource_purpose = 'users' class UserMeList(ListAPIView): @@ -1084,6 +1159,7 @@ class UserMeList(ListAPIView): serializer_class = serializers.UserSerializer name = _('Me') ordering = ('username',) + resource_purpose = 'current authenticated user' def get_queryset(self): return self.model.objects.filter(pk=self.request.user.pk) @@ -1093,6 +1169,7 @@ class UserTeamsList(SubListAPIView): model = models.Team serializer_class = serializers.TeamSerializer parent_model = models.User + resource_purpose = 'teams of a user' def get_queryset(self): u = get_object_or_404(models.User, pk=self.kwargs['pk']) @@ -1110,6 +1187,7 @@ class UserRolesList(SubListAttachDetachAPIView): relationship = 'roles' permission_classes = (IsAuthenticated,) search_fields = ('role_field', 'content_type__model') + resource_purpose = 'roles of a user' def get_queryset(self): u = get_object_or_404(models.User, pk=self.kwargs['pk']) @@ -1119,6 +1197,7 @@ class UserRolesList(SubListAttachDetachAPIView): return models.Role.filter_visible_roles(self.request.user, u.roles.all()).exclude(content_type=content_type, object_id=u.id) + @extend_schema_if_available(extensions={"x-ai-description": "Add a role to a user"}) def post(self, request, *args, **kwargs): sub_id = request.data.get('id', None) if not sub_id: @@ -1149,6 +1228,7 @@ class UserProjectsList(SubListAPIView): model = models.Project serializer_class = serializers.ProjectSerializer parent_model = models.User + resource_purpose = 'projects accessible to a user' def get_queryset(self): parent = self.get_parent_object() @@ -1162,6 +1242,7 @@ class UserOrganizationsList(OrganizationCountsMixin, SubListAPIView): model = models.Organization serializer_class = serializers.OrganizationSerializer parent_model = models.User + resource_purpose = 'organizations of a user' def get_queryset(self): parent = self.get_parent_object() @@ -1175,6 +1256,7 @@ class UserAdminOfOrganizationsList(OrganizationCountsMixin, SubListAPIView): model = models.Organization serializer_class = serializers.OrganizationSerializer parent_model = models.User + resource_purpose = 'organizations where user is admin' def get_queryset(self): parent = self.get_parent_object() @@ -1190,6 +1272,7 @@ class UserActivityStreamList(SubListAPIView): parent_model = models.User relationship = 'activitystream_set' search_fields = ('changes',) + resource_purpose = 'activity stream for a user' def get_queryset(self): parent = self.get_parent_object() @@ -1201,6 +1284,7 @@ class UserActivityStreamList(SubListAPIView): class UserDetail(RetrieveUpdateDestroyAPIView): model = models.User serializer_class = serializers.UserSerializer + resource_purpose = 'user detail' def update_filter(self, request, *args, **kwargs): '''make sure non-read-only fields that can only be edited by admins, are only edited by admins''' @@ -1238,16 +1322,19 @@ class UserDetail(RetrieveUpdateDestroyAPIView): class UserAccessList(ResourceAccessList): model = models.User # needs to be User for AccessLists's parent_model = models.User + resource_purpose = 'users who can access the user' class CredentialTypeList(ListCreateAPIView): model = models.CredentialType serializer_class = serializers.CredentialTypeSerializer + resource_purpose = 'credential types' class CredentialTypeDetail(RetrieveUpdateDestroyAPIView): model = models.CredentialType serializer_class = serializers.CredentialTypeSerializer + resource_purpose = 'credential type detail' def destroy(self, request, *args, **kwargs): instance = self.get_object() @@ -1263,19 +1350,31 @@ class CredentialTypeCredentialList(SubListCreateAPIView): parent_model = models.CredentialType relationship = 'credentials' serializer_class = serializers.CredentialSerializer + resource_purpose = 'credentials of a credential type' +@extend_schema_if_available(extensions={"x-ai-description": "Get activity stream for credential type"}) class CredentialTypeActivityStreamList(SubListAPIView): model = models.ActivityStream serializer_class = serializers.ActivityStreamSerializer parent_model = models.CredentialType relationship = 'activitystream_set' search_fields = ('changes',) + resource_purpose = 'activity stream for a credential type' class CredentialList(ListCreateAPIView): model = models.Credential serializer_class = serializers.CredentialSerializerCreate + resource_purpose = 'credentials' + + @extend_schema_if_available( + extensions={ + "x-ai-description": "Create a new credential. The `inputs` field contain type-specific input fields. The required fields depend on related `credential_type`. Use GET /v2/credential_types/{id}/ (tool name: controller.credential_types_retrieve) and inspect `inputs` field for the specific credential type's expected schema." + } + ) + def post(self, request, *args, **kwargs): + return super().post(request, *args, **kwargs) class CredentialOwnerUsersList(SubListAPIView): @@ -1284,12 +1383,14 @@ class CredentialOwnerUsersList(SubListAPIView): parent_model = models.Credential relationship = 'admin_role.members' ordering = ('username',) + resource_purpose = 'owner users of a credential' class CredentialOwnerTeamsList(SubListAPIView): model = models.Team serializer_class = serializers.TeamSerializer parent_model = models.Credential + resource_purpose = 'owner teams of a credential' def get_queryset(self): credential = get_object_or_404(self.parent_model, pk=self.kwargs['pk']) @@ -1306,6 +1407,7 @@ class UserCredentialsList(SubListCreateAPIView): model = models.Credential serializer_class = serializers.UserCredentialSerializerCreate parent_model = models.User + resource_purpose = 'credentials owned by a user' parent_key = 'user' def get_queryset(self): @@ -1322,6 +1424,7 @@ class TeamCredentialsList(SubListCreateAPIView): serializer_class = serializers.TeamCredentialSerializerCreate parent_model = models.Team parent_key = 'team' + resource_purpose = 'credentials owned by a team' def get_queryset(self): team = self.get_parent_object() @@ -1337,6 +1440,7 @@ class OrganizationCredentialList(SubListCreateAPIView): serializer_class = serializers.OrganizationCredentialSerializerCreate parent_model = models.Organization parent_key = 'organization' + resource_purpose = 'credentials of an organization' def get_queryset(self): organization = self.get_parent_object() @@ -1354,6 +1458,7 @@ class OrganizationCredentialList(SubListCreateAPIView): class CredentialDetail(RetrieveUpdateDestroyAPIView): model = models.Credential serializer_class = serializers.CredentialSerializer + resource_purpose = 'credential detail' def destroy(self, request, *args, **kwargs): instance = self.get_object() @@ -1362,17 +1467,21 @@ class CredentialDetail(RetrieveUpdateDestroyAPIView): return super(CredentialDetail, self).destroy(request, *args, **kwargs) +@extend_schema_if_available(extensions={"x-ai-description": "Get activity stream for a credential"}) class CredentialActivityStreamList(SubListAPIView): model = models.ActivityStream serializer_class = serializers.ActivityStreamSerializer parent_model = models.Credential relationship = 'activitystream_set' search_fields = ('changes',) + resource_purpose = 'activity stream for a credential' +@extend_schema_if_available(extensions={"x-ai-description": "Get access list for a credential"}) class CredentialAccessList(ResourceAccessList): model = models.User # needs to be User for AccessLists's parent_model = models.Credential + resource_purpose = 'users who can access the credential' class CredentialObjectRolesList(SubListAPIView): @@ -1382,6 +1491,7 @@ class CredentialObjectRolesList(SubListAPIView): parent_model = models.Credential search_fields = ('role_field', 'content_type__model') deprecated = True + resource_purpose = 'roles of a credential' def get_queryset(self): po = self.get_parent_object() @@ -1389,9 +1499,11 @@ class CredentialObjectRolesList(SubListAPIView): return models.Role.objects.filter(content_type=content_type, object_id=po.pk) +@extend_schema_if_available(extensions={"x-ai-description": "Copy credential"}) class CredentialCopy(CopyAPIView): model = models.Credential copy_return_serializer_class = serializers.CredentialSerializer + resource_purpose = 'copy of a credential' class CredentialExternalTest(SubDetailAPIView): @@ -1405,7 +1517,9 @@ class CredentialExternalTest(SubDetailAPIView): model = models.Credential serializer_class = serializers.EmptySerializer obj_permission_type = 'use' + resource_purpose = 'test external credential' + @extend_schema_if_available(extensions={"x-ai-description": "Test update the input values and metadata of an external credential"}) def post(self, request, *args, **kwargs): obj = self.get_object() backend_kwargs = {} @@ -1436,6 +1550,7 @@ class CredentialInputSourceDetail(RetrieveUpdateDestroyAPIView): model = models.CredentialInputSource serializer_class = serializers.CredentialInputSourceSerializer + resource_purpose = 'credential input source detail' class CredentialInputSourceList(ListCreateAPIView): @@ -1443,6 +1558,7 @@ class CredentialInputSourceList(ListCreateAPIView): model = models.CredentialInputSource serializer_class = serializers.CredentialInputSourceSerializer + resource_purpose = 'credential input sources' class CredentialInputSourceSubList(SubListCreateAPIView): @@ -1451,6 +1567,7 @@ class CredentialInputSourceSubList(SubListCreateAPIView): model = models.CredentialInputSource serializer_class = serializers.CredentialInputSourceSerializer parent_model = models.Credential + resource_purpose = 'input sources of a credential' relationship = 'input_sources' parent_key = 'target_credential' @@ -1465,7 +1582,9 @@ class CredentialTypeExternalTest(SubDetailAPIView): model = models.CredentialType serializer_class = serializers.EmptySerializer + resource_purpose = 'test external credential type' + @extend_schema_if_available(extensions={"x-ai-description": "Test a complete set of input values for an external credential"}) def post(self, request, *args, **kwargs): obj = self.get_object() backend_kwargs = request.data.get('inputs', {}) @@ -1500,6 +1619,7 @@ class HostMetricList(ListAPIView): serializer_class = serializers.HostMetricSerializer permission_classes = (IsSystemAdminOrAuditor,) search_fields = ('hostname', 'deleted') + resource_purpose = 'host metrics' def get_queryset(self): return self.model.objects.all() @@ -1510,7 +1630,9 @@ class HostMetricDetail(RetrieveDestroyAPIView): model = models.HostMetric serializer_class = serializers.HostMetricSerializer permission_classes = (IsSystemAdminOrAuditor,) + resource_purpose = 'host metric detail' + @extend_schema_if_available(extensions={"x-ai-description": "Soft delete a host metric"}) def delete(self, request, *args, **kwargs): self.get_object().soft_delete() @@ -1523,6 +1645,7 @@ class HostMetricSummaryMonthlyList(ListAPIView): serializer_class = serializers.HostMetricSummaryMonthlySerializer permission_classes = (IsSystemAdminOrAuditor,) search_fields = ('date',) + resource_purpose = 'monthly summaries for host metrics' def get_queryset(self): return self.model.objects.all() @@ -1532,6 +1655,7 @@ class HostList(HostRelatedSearchMixin, ListCreateAPIView): always_allow_superuser = False model = models.Host serializer_class = serializers.HostSerializer + resource_purpose = 'hosts' def get_queryset(self): qs = super(HostList, self).get_queryset() @@ -1552,7 +1676,9 @@ class HostDetail(RelatedJobsPreventDeleteMixin, RetrieveUpdateDestroyAPIView): always_allow_superuser = False model = models.Host serializer_class = serializers.HostSerializer + resource_purpose = 'host detail' + @extend_schema_if_available(extensions={"x-ai-description": "Delete a host"}) def delete(self, request, *args, **kwargs): if self.get_object().inventory.pending_deletion: return Response({"error": _("The inventory for this host is already being deleted.")}, status=status.HTTP_400_BAD_REQUEST) @@ -1564,7 +1690,9 @@ class HostDetail(RelatedJobsPreventDeleteMixin, RetrieveUpdateDestroyAPIView): class HostAnsibleFactsDetail(RetrieveAPIView): model = models.Host serializer_class = serializers.AnsibleFactsSerializer + resource_purpose = 'ansible facts of a host' + @extend_schema_if_available(extensions={"x-ai-description": "Get Ansible facts for a host"}) def get(self, request, *args, **kwargs): obj = self.get_object() if obj.inventory.kind == 'constructed': @@ -1581,6 +1709,7 @@ class InventoryHostsList(HostRelatedSearchMixin, SubListCreateAttachDetachAPIVie relationship = 'hosts' parent_key = 'inventory' filter_read_permission = False + resource_purpose = 'hosts of an inventory' class HostGroupsList(SubListCreateAttachDetachAPIView): @@ -1590,6 +1719,7 @@ class HostGroupsList(SubListCreateAttachDetachAPIView): serializer_class = serializers.GroupSerializer parent_model = models.Host relationship = 'groups' + resource_purpose = 'the list of groups a host is directly a member of' def update_raw_data(self, data): data.pop('inventory', None) @@ -1612,6 +1742,7 @@ class HostAllGroupsList(SubListAPIView): serializer_class = serializers.GroupSerializer parent_model = models.Host relationship = 'groups' + resource_purpose = 'the list of all groups of which the host is directly or indirectly a member of' def get_queryset(self): parent = self.get_parent_object() @@ -1626,6 +1757,7 @@ class HostInventorySourcesList(SubListAPIView): serializer_class = serializers.InventorySourceSerializer parent_model = models.Host relationship = 'inventory_sources' + resource_purpose = 'inventory sources of a host' class HostSmartInventoriesList(SubListAPIView): @@ -1633,6 +1765,7 @@ class HostSmartInventoriesList(SubListAPIView): serializer_class = serializers.InventorySerializer parent_model = models.Host relationship = 'smart_inventories' + resource_purpose = 'smart inventories of a host' class HostActivityStreamList(SubListAPIView): @@ -1641,6 +1774,7 @@ class HostActivityStreamList(SubListAPIView): parent_model = models.Host relationship = 'activitystream_set' search_fields = ('changes',) + resource_purpose = 'activity stream for a host' def get_queryset(self): parent = self.get_parent_object() @@ -1664,6 +1798,7 @@ class GatewayTimeout(APIException): class GroupList(ListCreateAPIView): model = models.Group serializer_class = serializers.GroupSerializer + resource_purpose = 'groups' class EnforceParentRelationshipMixin(object): @@ -1702,6 +1837,7 @@ class GroupChildrenList(EnforceParentRelationshipMixin, SubListCreateAttachDetac parent_model = models.Group relationship = 'children' enforce_parent_relationship = 'inventory' + resource_purpose = 'child groups' def unattach(self, request, *args, **kwargs): sub_id = request.data.get('id', None) @@ -1728,6 +1864,7 @@ class GroupPotentialChildrenList(SubListAPIView): model = models.Group serializer_class = serializers.GroupSerializer parent_model = models.Group + resource_purpose = 'potential children of group' def get_queryset(self): parent = self.get_parent_object() @@ -1747,6 +1884,7 @@ class GroupHostsList(HostRelatedSearchMixin, SubListCreateAttachDetachAPIView): serializer_class = serializers.HostSerializer parent_model = models.Group relationship = 'hosts' + resource_purpose = 'hosts of a group' def update_raw_data(self, data): data.pop('inventory', None) @@ -1772,6 +1910,7 @@ class GroupAllHostsList(HostRelatedSearchMixin, SubListAPIView): serializer_class = serializers.HostSerializer parent_model = models.Group relationship = 'hosts' + resource_purpose = 'all hosts of a group including subgroups' def get_queryset(self): parent = self.get_parent_object() @@ -1786,6 +1925,7 @@ class GroupInventorySourcesList(SubListAPIView): serializer_class = serializers.InventorySourceSerializer parent_model = models.Group relationship = 'inventory_sources' + resource_purpose = 'inventory sources of a group' class GroupActivityStreamList(SubListAPIView): @@ -1794,6 +1934,7 @@ class GroupActivityStreamList(SubListAPIView): parent_model = models.Group relationship = 'activitystream_set' search_fields = ('changes',) + resource_purpose = 'activity stream for a group' def get_queryset(self): parent = self.get_parent_object() @@ -1805,6 +1946,7 @@ class GroupActivityStreamList(SubListAPIView): class GroupDetail(RelatedJobsPreventDeleteMixin, RetrieveUpdateDestroyAPIView): model = models.Group serializer_class = serializers.GroupSerializer + resource_purpose = 'group detail' def destroy(self, request, *args, **kwargs): obj = self.get_object() @@ -1820,14 +1962,17 @@ class InventoryGroupsList(SubListCreateAttachDetachAPIView): parent_model = models.Inventory relationship = 'groups' parent_key = 'inventory' + resource_purpose = 'groups of an inventory' +@extend_schema_if_available(extensions={"x-ai-description": "List root (top-level) groups of an inventory"}) class InventoryRootGroupsList(SubListCreateAttachDetachAPIView): model = models.Group serializer_class = serializers.GroupSerializer parent_model = models.Inventory relationship = 'groups' parent_key = 'inventory' + resource_purpose = 'root groups of an inventory' def get_queryset(self): parent = self.get_parent_object() @@ -1840,28 +1985,34 @@ class BaseVariableData(RetrieveUpdateAPIView): parser_classes = api_settings.DEFAULT_PARSER_CLASSES + [YAMLParser] renderer_classes = api_settings.DEFAULT_RENDERER_CLASSES + [YAMLRenderer] permission_classes = (VariableDataPermission,) + resource_purpose = 'base view for variable data' class InventoryVariableData(BaseVariableData): model = models.Inventory serializer_class = serializers.InventoryVariableDataSerializer + resource_purpose = 'variable data for an inventory' class HostVariableData(BaseVariableData): model = models.Host serializer_class = serializers.HostVariableDataSerializer + resource_purpose = 'variable data for a host' class GroupVariableData(BaseVariableData): model = models.Group serializer_class = serializers.GroupVariableDataSerializer + resource_purpose = 'variable data for a group' +@extend_schema_if_available(extensions={"x-ai-description": "Generate inventory group and host data as needed for an inventory script"}) class InventoryScriptView(RetrieveAPIView): model = models.Inventory serializer_class = serializers.InventoryScriptSerializer permission_classes = (TaskPermission,) filter_backends = () + resource_purpose = 'inventory script data' def retrieve(self, request, *args, **kwargs): obj = self.get_object() @@ -1888,10 +2039,12 @@ class InventoryScriptView(RetrieveAPIView): return Response(obj.get_script_data(hostvars=hostvars, towervars=towervars, show_all=show_all, slice_number=slice_number, slice_count=slice_count)) +@extend_schema_if_available(extensions={"x-ai-description": "Create group tree for an inventory"}) class InventoryTreeView(RetrieveAPIView): model = models.Inventory serializer_class = serializers.GroupTreeSerializer filter_backends = () + resource_purpose = 'inventory group tree' def _populate_group_children(self, group_data, all_group_data_map, group_children_map): if 'children' in group_data: @@ -1927,6 +2080,7 @@ class InventoryInventorySourcesList(SubListCreateAPIView): always_allow_superuser = False relationship = 'inventory_sources' parent_key = 'inventory' + resource_purpose = 'inventory sources' class InventoryInventorySourcesUpdate(RetrieveAPIView): @@ -1936,7 +2090,9 @@ class InventoryInventorySourcesUpdate(RetrieveAPIView): obj_permission_type = 'start' serializer_class = serializers.InventorySourceUpdateSerializer permission_classes = (InventoryInventorySourcesUpdatePermission,) + resource_purpose = 'update inventory sources' + @extend_schema_if_available(extensions={"x-ai-description": "Determine if any of the sources of this inventory can be updated"}) def retrieve(self, request, *args, **kwargs): inventory = self.get_object() update_data = [] @@ -1945,6 +2101,7 @@ class InventoryInventorySourcesUpdate(RetrieveAPIView): update_data.append(details) return Response(update_data) + @extend_schema_if_available(extensions={"x-ai-description": "Update inventory sources"}) def post(self, request, *args, **kwargs): inventory = self.get_object() update_data = [] @@ -1980,11 +2137,13 @@ class InventorySourceList(ListCreateAPIView): model = models.InventorySource serializer_class = serializers.InventorySourceSerializer always_allow_superuser = False + resource_purpose = 'inventory sources' class InventorySourceDetail(RelatedJobsPreventDeleteMixin, RetrieveUpdateDestroyAPIView): model = models.InventorySource serializer_class = serializers.InventorySourceSerializer + resource_purpose = 'inventory source detail' class InventorySourceSchedulesList(SubListCreateAPIView): @@ -1995,6 +2154,7 @@ class InventorySourceSchedulesList(SubListCreateAPIView): parent_model = models.InventorySource relationship = 'schedules' parent_key = 'unified_job_template' + resource_purpose = 'schedules of an inventory source' class InventorySourceActivityStreamList(SubListAPIView): @@ -2003,13 +2163,16 @@ class InventorySourceActivityStreamList(SubListAPIView): parent_model = models.InventorySource relationship = 'activitystream_set' search_fields = ('changes',) + resource_purpose = 'activity stream of an inventory source' class InventorySourceNotificationTemplatesAnyList(SubListCreateAttachDetachAPIView): model = models.NotificationTemplate serializer_class = serializers.NotificationTemplateSerializer parent_model = models.InventorySource + resource_purpose = 'base view for notification templates of an inventory source' + @extend_schema_if_available(extensions={"x-ai-description": "Add a notification template to an inventory source"}) def post(self, request, *args, **kwargs): parent = self.get_parent_object() if parent.source not in compute_cloud_inventory_sources(): @@ -2022,14 +2185,17 @@ class InventorySourceNotificationTemplatesAnyList(SubListCreateAttachDetachAPIVi class InventorySourceNotificationTemplatesStartedList(InventorySourceNotificationTemplatesAnyList): relationship = 'notification_templates_started' + resource_purpose = 'notification templates triggered on inventory source update start' class InventorySourceNotificationTemplatesErrorList(InventorySourceNotificationTemplatesAnyList): relationship = 'notification_templates_error' + resource_purpose = 'notification templates triggered on inventory source update error' class InventorySourceNotificationTemplatesSuccessList(InventorySourceNotificationTemplatesAnyList): relationship = 'notification_templates_success' + resource_purpose = 'notification templates triggered on inventory source update success' class InventorySourceHostsList(HostRelatedSearchMixin, SubListDestroyAPIView): @@ -2038,6 +2204,7 @@ class InventorySourceHostsList(HostRelatedSearchMixin, SubListDestroyAPIView): parent_model = models.InventorySource relationship = 'hosts' check_sub_obj_permission = False + resource_purpose = 'hosts of an inventory source' def perform_list_destroy(self, instance_list): inv_source = self.get_parent_object() @@ -2066,6 +2233,7 @@ class InventorySourceGroupsList(SubListDestroyAPIView): parent_model = models.InventorySource relationship = 'groups' check_sub_obj_permission = False + resource_purpose = 'groups of an inventory source' def perform_list_destroy(self, instance_list): inv_source = self.get_parent_object() @@ -2090,6 +2258,7 @@ class InventorySourceUpdatesList(SubListAPIView): serializer_class = serializers.InventoryUpdateListSerializer parent_model = models.InventorySource relationship = 'inventory_updates' + resource_purpose = 'inventory updates of an inventory source' class InventorySourceCredentialsList(SubListAttachDetachAPIView): @@ -2097,6 +2266,7 @@ class InventorySourceCredentialsList(SubListAttachDetachAPIView): model = models.Credential serializer_class = serializers.CredentialSerializer relationship = 'credentials' + resource_purpose = 'credentials of an inventory source' def is_valid_relation(self, parent, sub, created=False): # Inventory source credentials are exclusive with all other credentials @@ -2114,7 +2284,9 @@ class InventorySourceUpdateView(RetrieveAPIView): model = models.InventorySource obj_permission_type = 'start' serializer_class = serializers.InventorySourceUpdateSerializer + resource_purpose = 'update an inventory source' + @extend_schema_if_available(extensions={"x-ai-description": "Update inventory source"}) def post(self, request, *args, **kwargs): obj = self.get_object() serializer = self.get_serializer(instance=obj, data=request.data) @@ -2136,11 +2308,13 @@ class InventorySourceUpdateView(RetrieveAPIView): class InventoryUpdateList(ListAPIView): model = models.InventoryUpdate serializer_class = serializers.InventoryUpdateListSerializer + resource_purpose = 'inventory updates' class InventoryUpdateDetail(UnifiedJobDeletionMixin, RetrieveDestroyAPIView): model = models.InventoryUpdate serializer_class = serializers.InventoryUpdateDetailSerializer + resource_purpose = 'inventory update detail' class InventoryUpdateCredentialsList(SubListAPIView): @@ -2148,11 +2322,13 @@ class InventoryUpdateCredentialsList(SubListAPIView): model = models.Credential serializer_class = serializers.CredentialSerializer relationship = 'credentials' + resource_purpose = 'credentials of an inventory update' class InventoryUpdateCancel(GenericCancelView): model = models.InventoryUpdate serializer_class = serializers.InventoryUpdateCancelSerializer + resource_purpose = 'cancel for an inventory update' class InventoryUpdateNotificationsList(SubListAPIView): @@ -2161,12 +2337,14 @@ class InventoryUpdateNotificationsList(SubListAPIView): parent_model = models.InventoryUpdate relationship = 'notifications' search_fields = ('subject', 'notification_type', 'body') + resource_purpose = 'notifications of an inventory update' class JobTemplateList(ListCreateAPIView): model = models.JobTemplate serializer_class = serializers.JobTemplateSerializer always_allow_superuser = False + resource_purpose = 'job templates' def check_permissions(self, request): if request.method == 'POST': @@ -2184,6 +2362,7 @@ class JobTemplateDetail(RelatedJobsPreventDeleteMixin, RetrieveUpdateDestroyAPIV model = models.JobTemplate serializer_class = serializers.JobTemplateSerializer always_allow_superuser = False + resource_purpose = 'job template detail' class JobTemplateLaunch(RetrieveAPIView): @@ -2191,6 +2370,7 @@ class JobTemplateLaunch(RetrieveAPIView): obj_permission_type = 'start' serializer_class = serializers.JobLaunchSerializer always_allow_superuser = False + resource_purpose = 'launch a job from a job template' def update_raw_data(self, data): try: @@ -2243,6 +2423,9 @@ class JobTemplateLaunch(RetrieveAPIView): return modern_data + @extend_schema_if_available( + extensions={'x-ai-description': 'Launch a job'}, + ) def post(self, request, *args, **kwargs): obj = self.get_object() @@ -2311,17 +2494,21 @@ class JobTemplateSchedulesList(SubListCreateAPIView): parent_model = models.JobTemplate relationship = 'schedules' parent_key = 'unified_job_template' + resource_purpose = 'schedules of a job template' class JobTemplateSurveySpec(GenericAPIView): model = models.JobTemplate obj_permission_type = 'admin' serializer_class = serializers.EmptySerializer + resource_purpose = 'job template survey specification' + @extend_schema_if_available(extensions={"x-ai-description": "Get job template survey specification"}) def get(self, request, *args, **kwargs): obj = self.get_object() return Response(obj.display_survey_spec()) + @extend_schema_if_available(extensions={"x-ai-description": "Update job template survey specification"}) def post(self, request, *args, **kwargs): obj = self.get_object() @@ -2481,6 +2668,7 @@ class JobTemplateSurveySpec(GenericAPIView): # Submission provides new encrypted default survey_item['default'] = encrypt_value(survey_item['default']) + @extend_schema_if_available(extensions={"x-ai-description": "Delete job template survey specification"}) def delete(self, request, *args, **kwargs): obj = self.get_object() if not request.user.can_access(self.model, 'delete', obj): @@ -2492,6 +2680,7 @@ class JobTemplateSurveySpec(GenericAPIView): class WorkflowJobTemplateSurveySpec(JobTemplateSurveySpec): model = models.WorkflowJobTemplate + resource_purpose = 'workflow job template survey specification' class JobTemplateActivityStreamList(SubListAPIView): @@ -2500,24 +2689,29 @@ class JobTemplateActivityStreamList(SubListAPIView): parent_model = models.JobTemplate relationship = 'activitystream_set' search_fields = ('changes',) + resource_purpose = 'activity stream of a job template' class JobTemplateNotificationTemplatesAnyList(SubListCreateAttachDetachAPIView): model = models.NotificationTemplate serializer_class = serializers.NotificationTemplateSerializer parent_model = models.JobTemplate + resource_purpose = 'base view for notification templates of a job template' class JobTemplateNotificationTemplatesStartedList(JobTemplateNotificationTemplatesAnyList): relationship = 'notification_templates_started' + resource_purpose = 'notification templates triggered on job start' class JobTemplateNotificationTemplatesErrorList(JobTemplateNotificationTemplatesAnyList): relationship = 'notification_templates_error' + resource_purpose = 'notification templates triggered on job error' class JobTemplateNotificationTemplatesSuccessList(JobTemplateNotificationTemplatesAnyList): relationship = 'notification_templates_success' + resource_purpose = 'notification templates triggered on job success' class JobTemplateCredentialsList(SubListCreateAttachDetachAPIView): @@ -2526,6 +2720,7 @@ class JobTemplateCredentialsList(SubListCreateAttachDetachAPIView): parent_model = models.JobTemplate relationship = 'credentials' filter_read_permission = False + resource_purpose = 'credentials of a job template' def is_valid_relation(self, parent, sub, created=False): if sub.unique_hash() in [cred.unique_hash() for cred in parent.credentials.all()]: @@ -2539,6 +2734,7 @@ class JobTemplateCredentialsList(SubListCreateAttachDetachAPIView): class JobTemplateLabelList(LabelSubListCreateAttachDetachView): parent_model = models.JobTemplate + resource_purpose = 'labels of a job template' class JobTemplateCallback(GenericAPIView): @@ -2546,6 +2742,7 @@ class JobTemplateCallback(GenericAPIView): permission_classes = (JobTemplateCallbackPermission,) serializer_class = serializers.EmptySerializer parser_classes = api_settings.DEFAULT_PARSER_CLASSES + [FormParser] + resource_purpose = 'job template provisioning callback' @csrf_exempt @transaction.non_atomic_requests @@ -2605,6 +2802,7 @@ class JobTemplateCallback(GenericAPIView): pass return matches + @extend_schema_if_available(extensions={"x-ai-description": "Get job template callback configuration"}) def get(self, request, *args, **kwargs): job_template = self.get_object() matching_hosts = self.find_matching_hosts() @@ -2614,6 +2812,7 @@ class JobTemplateCallback(GenericAPIView): data['request_meta'] = d return Response(data) + @extend_schema_if_available(extensions={"x-ai-description": "Trigger job template via provisioning callback"}) def post(self, request, *args, **kwargs): extra_vars = None # Be careful here: content_type can look like '; charset=blar' @@ -2695,6 +2894,7 @@ class JobTemplateJobsList(SubListAPIView): parent_model = models.JobTemplate relationship = 'jobs' parent_key = 'job_template' + resource_purpose = 'jobs of a job template' class JobTemplateSliceWorkflowJobsList(SubListCreateAPIView): @@ -2703,6 +2903,7 @@ class JobTemplateSliceWorkflowJobsList(SubListCreateAPIView): parent_model = models.JobTemplate relationship = 'slice_workflow_jobs' parent_key = 'job_template' + resource_purpose = 'slice workflow jobs of a job template' class JobTemplateInstanceGroupsList(SubListAttachDetachAPIView): @@ -2711,11 +2912,13 @@ class JobTemplateInstanceGroupsList(SubListAttachDetachAPIView): parent_model = models.JobTemplate relationship = 'instance_groups' filter_read_permission = False + resource_purpose = 'instance groups of a job template' class JobTemplateAccessList(ResourceAccessList): model = models.User # needs to be User for AccessLists's parent_model = models.JobTemplate + resource_purpose = 'users who can access a job template' class JobTemplateObjectRolesList(SubListAPIView): @@ -2725,6 +2928,7 @@ class JobTemplateObjectRolesList(SubListAPIView): parent_model = models.JobTemplate search_fields = ('role_field', 'content_type__model') deprecated = True + resource_purpose = 'roles of a job template' def get_queryset(self): po = self.get_parent_object() @@ -2735,17 +2939,20 @@ class JobTemplateObjectRolesList(SubListAPIView): class JobTemplateCopy(CopyAPIView): model = models.JobTemplate copy_return_serializer_class = serializers.JobTemplateSerializer + resource_purpose = 'copy a job template' class WorkflowJobNodeList(ListAPIView): model = models.WorkflowJobNode serializer_class = serializers.WorkflowJobNodeListSerializer search_fields = ('unified_job_template__name', 'unified_job_template__description') + resource_purpose = 'workflow job nodes' class WorkflowJobNodeDetail(RetrieveAPIView): model = models.WorkflowJobNode serializer_class = serializers.WorkflowJobNodeDetailSerializer + resource_purpose = 'workflow job node detail' class WorkflowJobNodeCredentialsList(SubListAPIView): @@ -2753,6 +2960,7 @@ class WorkflowJobNodeCredentialsList(SubListAPIView): serializer_class = serializers.CredentialSerializer parent_model = models.WorkflowJobNode relationship = 'credentials' + resource_purpose = 'credentials of a workflow job node' class WorkflowJobNodeLabelsList(SubListAPIView): @@ -2760,6 +2968,7 @@ class WorkflowJobNodeLabelsList(SubListAPIView): serializer_class = serializers.LabelSerializer parent_model = models.WorkflowJobNode relationship = 'labels' + resource_purpose = 'labels of a workflow job node' class WorkflowJobNodeInstanceGroupsList(SubListAttachDetachAPIView): @@ -2767,25 +2976,30 @@ class WorkflowJobNodeInstanceGroupsList(SubListAttachDetachAPIView): serializer_class = serializers.InstanceGroupSerializer parent_model = models.WorkflowJobNode relationship = 'instance_groups' + resource_purpose = 'instance groups of a workflow job node' class WorkflowJobTemplateNodeList(ListCreateAPIView): model = models.WorkflowJobTemplateNode serializer_class = serializers.WorkflowJobTemplateNodeSerializer search_fields = ('unified_job_template__name', 'unified_job_template__description') + resource_purpose = 'workflow job template nodes' class WorkflowJobTemplateNodeDetail(RetrieveUpdateDestroyAPIView): model = models.WorkflowJobTemplateNode serializer_class = serializers.WorkflowJobTemplateNodeDetailSerializer + resource_purpose = 'workflow job template node detail' class WorkflowJobTemplateNodeCredentialsList(LaunchConfigCredentialsBase): parent_model = models.WorkflowJobTemplateNode + resource_purpose = 'credentials of a workflow job template node' class WorkflowJobTemplateNodeLabelsList(LabelSubListCreateAttachDetachView): parent_model = models.WorkflowJobTemplateNode + resource_purpose = 'labels of a workflow job template node' class WorkflowJobTemplateNodeInstanceGroupsList(SubListAttachDetachAPIView): @@ -2793,6 +3007,7 @@ class WorkflowJobTemplateNodeInstanceGroupsList(SubListAttachDetachAPIView): serializer_class = serializers.InstanceGroupSerializer parent_model = models.WorkflowJobTemplateNode relationship = 'instance_groups' + resource_purpose = 'instance groups of a workflow job template node' class WorkflowJobTemplateNodeChildrenBaseList(EnforceParentRelationshipMixin, SubListCreateAttachDetachAPIView): @@ -2804,6 +3019,7 @@ class WorkflowJobTemplateNodeChildrenBaseList(EnforceParentRelationshipMixin, Su enforce_parent_relationship = 'workflow_job_template' search_fields = ('unified_job_template__name', 'unified_job_template__description') filter_read_permission = False + resource_purpose = 'base view for child nodes of a workflow job template node' def is_valid_relation(self, parent, sub, created=False): if created: @@ -2838,7 +3054,9 @@ class WorkflowJobTemplateNodeCreateApproval(RetrieveAPIView): model = models.WorkflowJobTemplateNode serializer_class = serializers.WorkflowJobTemplateNodeCreateApprovalSerializer permission_classes = [] + resource_purpose = 'create an approval node for a workflow job template node' + @extend_schema_if_available(extensions={"x-ai-description": "Create an approval node for a workflow job template node"}) def post(self, request, *args, **kwargs): obj = self.get_object() serializer = self.get_serializer(instance=obj, data=request.data) @@ -2862,14 +3080,17 @@ class WorkflowJobTemplateNodeCreateApproval(RetrieveAPIView): class WorkflowJobTemplateNodeSuccessNodesList(WorkflowJobTemplateNodeChildrenBaseList): relationship = 'success_nodes' + resource_purpose = 'success nodes of a workflow job template node' class WorkflowJobTemplateNodeFailureNodesList(WorkflowJobTemplateNodeChildrenBaseList): relationship = 'failure_nodes' + resource_purpose = 'failure nodes of a workflow job template node' class WorkflowJobTemplateNodeAlwaysNodesList(WorkflowJobTemplateNodeChildrenBaseList): relationship = 'always_nodes' + resource_purpose = 'always nodes of a workflow job template node' class WorkflowJobNodeChildrenBaseList(SubListAPIView): @@ -2879,24 +3100,29 @@ class WorkflowJobNodeChildrenBaseList(SubListAPIView): relationship = '' search_fields = ('unified_job_template__name', 'unified_job_template__description') filter_read_permission = False + resource_purpose = 'base view for child nodes of a workflow job node' class WorkflowJobNodeSuccessNodesList(WorkflowJobNodeChildrenBaseList): relationship = 'success_nodes' + resource_purpose = 'success nodes of a workflow job node' class WorkflowJobNodeFailureNodesList(WorkflowJobNodeChildrenBaseList): relationship = 'failure_nodes' + resource_purpose = 'failure nodes of a workflow job node' class WorkflowJobNodeAlwaysNodesList(WorkflowJobNodeChildrenBaseList): relationship = 'always_nodes' + resource_purpose = 'always nodes of a workflow job node' class WorkflowJobTemplateList(ListCreateAPIView): model = models.WorkflowJobTemplate serializer_class = serializers.WorkflowJobTemplateSerializer always_allow_superuser = False + resource_purpose = 'workflow job templates' def check_permissions(self, request): if request.method == 'POST': @@ -2914,12 +3140,15 @@ class WorkflowJobTemplateDetail(RelatedJobsPreventDeleteMixin, RetrieveUpdateDes model = models.WorkflowJobTemplate serializer_class = serializers.WorkflowJobTemplateSerializer always_allow_superuser = False + resource_purpose = 'workflow job template detail' class WorkflowJobTemplateCopy(CopyAPIView): model = models.WorkflowJobTemplate copy_return_serializer_class = serializers.WorkflowJobTemplateSerializer + resource_purpose = 'copy a workflow job template' + @extend_schema_if_available(extensions={"x-ai-description": "Get workflow job template copy status"}) def get(self, request, *args, **kwargs): obj = self.get_object() if not request.user.can_access(obj.__class__, 'read', obj): @@ -2972,6 +3201,7 @@ class WorkflowJobTemplateCopy(CopyAPIView): class WorkflowJobTemplateLabelList(JobTemplateLabelList): parent_model = models.WorkflowJobTemplate + resource_purpose = 'labels of a workflow job template' class WorkflowJobTemplateLaunch(RetrieveAPIView): @@ -2979,6 +3209,7 @@ class WorkflowJobTemplateLaunch(RetrieveAPIView): obj_permission_type = 'start' serializer_class = serializers.WorkflowJobLaunchSerializer always_allow_superuser = False + resource_purpose = 'launch a workflow job from a workflow job template' def update_raw_data(self, data): try: @@ -3006,6 +3237,9 @@ class WorkflowJobTemplateLaunch(RetrieveAPIView): return data + @extend_schema_if_available( + extensions={'x-ai-description': 'Launch a workflow job'}, + ) def post(self, request, *args, **kwargs): obj = self.get_object() @@ -3034,6 +3268,7 @@ class WorkflowJobRelaunch(GenericAPIView): model = models.WorkflowJob obj_permission_type = 'start' serializer_class = serializers.EmptySerializer + resource_purpose = 'relaunch a workflow job' def check_object_permissions(self, request, obj): if request.method == 'POST' and obj: @@ -3042,9 +3277,13 @@ class WorkflowJobRelaunch(GenericAPIView): self.permission_denied(request, message=messages['workflow_job_template']) return super(WorkflowJobRelaunch, self).check_object_permissions(request, obj) + @extend_schema_if_available(extensions={"x-ai-description": "Get workflow job relaunch information"}) def get(self, request, *args, **kwargs): return Response({}) + @extend_schema_if_available( + extensions={'x-ai-description': 'Relaunch a workflow job'}, + ) def post(self, request, *args, **kwargs): obj = self.get_object() if obj.is_sliced_job: @@ -3070,6 +3309,7 @@ class WorkflowJobTemplateWorkflowNodesList(SubListCreateAPIView): search_fields = ('unified_job_template__name', 'unified_job_template__description') ordering = ('id',) # assure ordering by id for consistency filter_read_permission = False + resource_purpose = 'workflow nodes of a workflow job template' class WorkflowJobTemplateJobsList(SubListAPIView): @@ -3078,6 +3318,7 @@ class WorkflowJobTemplateJobsList(SubListAPIView): parent_model = models.WorkflowJobTemplate relationship = 'workflow_jobs' parent_key = 'workflow_job_template' + resource_purpose = 'workflow jobs of a workflow job template' class WorkflowJobTemplateSchedulesList(SubListCreateAPIView): @@ -3088,33 +3329,40 @@ class WorkflowJobTemplateSchedulesList(SubListCreateAPIView): parent_model = models.WorkflowJobTemplate relationship = 'schedules' parent_key = 'unified_job_template' + resource_purpose = 'schedules of a workflow job template' class WorkflowJobTemplateNotificationTemplatesAnyList(SubListCreateAttachDetachAPIView): model = models.NotificationTemplate serializer_class = serializers.NotificationTemplateSerializer parent_model = models.WorkflowJobTemplate + resource_purpose = 'base view for notification templates of a workflow job template' class WorkflowJobTemplateNotificationTemplatesStartedList(WorkflowJobTemplateNotificationTemplatesAnyList): relationship = 'notification_templates_started' + resource_purpose = 'notification templates triggered on workflow job start' class WorkflowJobTemplateNotificationTemplatesErrorList(WorkflowJobTemplateNotificationTemplatesAnyList): relationship = 'notification_templates_error' + resource_purpose = 'notification templates triggered on workflow job error' class WorkflowJobTemplateNotificationTemplatesSuccessList(WorkflowJobTemplateNotificationTemplatesAnyList): relationship = 'notification_templates_success' + resource_purpose = 'notification templates triggered on workflow job success' class WorkflowJobTemplateNotificationTemplatesApprovalList(WorkflowJobTemplateNotificationTemplatesAnyList): relationship = 'notification_templates_approvals' + resource_purpose = 'notification templates triggered on workflow approval' class WorkflowJobTemplateAccessList(ResourceAccessList): model = models.User # needs to be User for AccessLists's parent_model = models.WorkflowJobTemplate + resource_purpose = 'users who can access a workflow job template' class WorkflowJobTemplateObjectRolesList(SubListAPIView): @@ -3124,6 +3372,7 @@ class WorkflowJobTemplateObjectRolesList(SubListAPIView): parent_model = models.WorkflowJobTemplate search_fields = ('role_field', 'content_type__model') deprecated = True + resource_purpose = 'roles of a workflow job template' def get_queryset(self): po = self.get_parent_object() @@ -3137,6 +3386,7 @@ class WorkflowJobTemplateActivityStreamList(SubListAPIView): parent_model = models.WorkflowJobTemplate relationship = 'activitystream_set' search_fields = ('changes',) + resource_purpose = 'activity stream of a workflow job template' def get_queryset(self): parent = self.get_parent_object() @@ -3148,11 +3398,13 @@ class WorkflowJobTemplateActivityStreamList(SubListAPIView): class WorkflowJobList(ListAPIView): model = models.WorkflowJob serializer_class = serializers.WorkflowJobListSerializer + resource_purpose = 'workflow jobs' class WorkflowJobDetail(UnifiedJobDeletionMixin, RetrieveDestroyAPIView): model = models.WorkflowJob serializer_class = serializers.WorkflowJobSerializer + resource_purpose = 'workflow job detail' class WorkflowJobWorkflowNodesList(SubListAPIView): @@ -3165,12 +3417,15 @@ class WorkflowJobWorkflowNodesList(SubListAPIView): search_fields = ('unified_job_template__name', 'unified_job_template__description') ordering = ('id',) # assure ordering by id for consistency filter_read_permission = False + resource_purpose = 'workflow nodes of a workflow job' class WorkflowJobCancel(GenericCancelView): model = models.WorkflowJob serializer_class = serializers.WorkflowJobCancelSerializer + resource_purpose = 'cancel for a workflow job' + @extend_schema_if_available(extensions={"x-ai-description": "Cancel a workflow job"}) def post(self, request, *args, **kwargs): r = super().post(request, *args, **kwargs) ScheduleWorkflowManager().schedule() @@ -3183,6 +3438,7 @@ class WorkflowJobNotificationsList(SubListAPIView): parent_model = models.WorkflowJob relationship = 'notifications' search_fields = ('subject', 'notification_type', 'body') + resource_purpose = 'notifications of a workflow job' def get_sublist_queryset(self, parent): return self.model.objects.filter( @@ -3197,12 +3453,15 @@ class WorkflowJobActivityStreamList(SubListAPIView): parent_model = models.WorkflowJob relationship = 'activitystream_set' search_fields = ('changes',) + resource_purpose = 'activity stream of a workflow job' class SystemJobTemplateList(ListAPIView): model = models.SystemJobTemplate serializer_class = serializers.SystemJobTemplateSerializer + resource_purpose = 'system job templates' + @extend_schema_if_available(extensions={"x-ai-description": "List system job templates"}) def get(self, request, *args, **kwargs): if not request.user.is_superuser and not request.user.is_system_auditor: raise PermissionDenied(_("Superuser privileges needed.")) @@ -3212,16 +3471,22 @@ class SystemJobTemplateList(ListAPIView): class SystemJobTemplateDetail(RetrieveAPIView): model = models.SystemJobTemplate serializer_class = serializers.SystemJobTemplateSerializer + resource_purpose = 'system job template detail' class SystemJobTemplateLaunch(GenericAPIView): model = models.SystemJobTemplate obj_permission_type = 'start' serializer_class = serializers.EmptySerializer + resource_purpose = 'launch a system job from a system job template' + @extend_schema_if_available(extensions={"x-ai-description": "Get system job template launch information"}) def get(self, request, *args, **kwargs): return Response({}) + @extend_schema_if_available( + extensions={'x-ai-description': 'Launch a system job'}, + ) def post(self, request, *args, **kwargs): obj = self.get_object() @@ -3242,6 +3507,7 @@ class SystemJobTemplateSchedulesList(SubListCreateAPIView): parent_model = models.SystemJobTemplate relationship = 'schedules' parent_key = 'unified_job_template' + resource_purpose = 'schedules of a system job template' class SystemJobTemplateJobsList(SubListAPIView): @@ -3250,34 +3516,41 @@ class SystemJobTemplateJobsList(SubListAPIView): parent_model = models.SystemJobTemplate relationship = 'jobs' parent_key = 'system_job_template' + resource_purpose = 'system jobs of a system job template' class SystemJobTemplateNotificationTemplatesAnyList(SubListCreateAttachDetachAPIView): model = models.NotificationTemplate serializer_class = serializers.NotificationTemplateSerializer parent_model = models.SystemJobTemplate + resource_purpose = 'base view for notification templates of a system job template' class SystemJobTemplateNotificationTemplatesStartedList(SystemJobTemplateNotificationTemplatesAnyList): relationship = 'notification_templates_started' + resource_purpose = 'notification templates triggered on system job start' class SystemJobTemplateNotificationTemplatesErrorList(SystemJobTemplateNotificationTemplatesAnyList): relationship = 'notification_templates_error' + resource_purpose = 'notification templates triggered on system job error' class SystemJobTemplateNotificationTemplatesSuccessList(SystemJobTemplateNotificationTemplatesAnyList): relationship = 'notification_templates_success' + resource_purpose = 'notification templates triggered on system job success' class JobList(ListAPIView): model = models.Job serializer_class = serializers.JobListSerializer + resource_purpose = 'jobs' class JobDetail(UnifiedJobDeletionMixin, RetrieveDestroyAPIView): model = models.Job serializer_class = serializers.JobDetailSerializer + resource_purpose = 'job detail' def update(self, request, *args, **kwargs): obj = self.get_object() @@ -3292,6 +3565,7 @@ class JobCredentialsList(SubListAPIView): serializer_class = serializers.CredentialSerializer parent_model = models.Job relationship = 'credentials' + resource_purpose = 'credentials of a job' class JobLabelList(SubListAPIView): @@ -3299,10 +3573,12 @@ class JobLabelList(SubListAPIView): serializer_class = serializers.LabelSerializer parent_model = models.Job relationship = 'labels' + resource_purpose = 'labels of a job' class WorkflowJobLabelList(JobLabelList): parent_model = models.WorkflowJob + resource_purpose = 'labels of a workflow job' class JobActivityStreamList(SubListAPIView): @@ -3311,17 +3587,20 @@ class JobActivityStreamList(SubListAPIView): parent_model = models.Job relationship = 'activitystream_set' search_fields = ('changes',) + resource_purpose = 'activity stream of a job' class JobCancel(GenericCancelView): model = models.Job serializer_class = serializers.JobCancelSerializer + resource_purpose = 'cancel for a job' class JobRelaunch(RetrieveAPIView): model = models.Job obj_permission_type = 'start' serializer_class = serializers.JobRelaunchSerializer + resource_purpose = 'relaunch a job' def update_raw_data(self, data): data = super(JobRelaunch, self).update_raw_data(data) @@ -3350,6 +3629,9 @@ class JobRelaunch(RetrieveAPIView): self.permission_denied(request, message=messages['detail']) return super(JobRelaunch, self).check_object_permissions(request, obj) + @extend_schema_if_available( + extensions={'x-ai-description': 'Relaunch a job'}, + ) def post(self, request, *args, **kwargs): obj = self.get_object() context = self.get_serializer_context() @@ -3409,7 +3691,9 @@ class JobCreateSchedule(RetrieveAPIView): model = models.Job obj_permission_type = 'start' serializer_class = serializers.JobCreateScheduleSerializer + resource_purpose = 'create a schedule from a job' + @extend_schema_if_available(extensions={"x-ai-description": "Create a schedule from a job"}) def post(self, request, *args, **kwargs): obj = self.get_object() @@ -3471,6 +3755,7 @@ class JobNotificationsList(SubListAPIView): parent_model = models.Job relationship = 'notifications' search_fields = ('subject', 'notification_type', 'body') + resource_purpose = 'notifications of a job' class BaseJobHostSummariesList(SubListAPIView): @@ -3481,27 +3766,33 @@ class BaseJobHostSummariesList(SubListAPIView): name = _('Job Host Summaries List') search_fields = ('host_name',) filter_read_permission = False + resource_purpose = 'base view for job host summaries' class HostJobHostSummariesList(BaseJobHostSummariesList): parent_model = models.Host + resource_purpose = 'job summaries of a host' class GroupJobHostSummariesList(BaseJobHostSummariesList): parent_model = models.Group + resource_purpose = 'job host summaries for a group' class JobJobHostSummariesList(BaseJobHostSummariesList): parent_model = models.Job + resource_purpose = 'job host summaries of a job' class JobHostSummaryDetail(RetrieveAPIView): model = models.JobHostSummary serializer_class = serializers.JobHostSummarySerializer + resource_purpose = 'job host summary detail' class JobEventDetail(RetrieveAPIView): serializer_class = serializers.JobEventSerializer + resource_purpose = 'job event detail' @property def is_partitioned(self): @@ -3526,6 +3817,7 @@ class JobEventChildrenList(NoTruncateMixin, SubListAPIView): relationship = 'children' name = _('Job Event Children List') search_fields = ('stdout',) + resource_purpose = 'child events of a job event' @property def is_partitioned(self): @@ -3556,6 +3848,7 @@ class BaseJobEventsList(NoTruncateMixin, SubListAPIView): relationship = 'job_events' name = _('Job Events List') search_fields = ('stdout',) + resource_purpose = 'base view for job events' def finalize_response(self, request, response, *args, **kwargs): response['X-UI-Max-Events'] = settings.MAX_UI_JOB_EVENTS @@ -3564,6 +3857,7 @@ class BaseJobEventsList(NoTruncateMixin, SubListAPIView): class HostJobEventsList(BaseJobEventsList): parent_model = models.Host + resource_purpose = 'job events of a host' def get_queryset(self): parent_obj = self.get_parent_object() @@ -3574,11 +3868,13 @@ class HostJobEventsList(BaseJobEventsList): class GroupJobEventsList(BaseJobEventsList): parent_model = models.Group + resource_purpose = 'job events for a group' class JobJobEventsList(BaseJobEventsList): parent_model = models.Job pagination_class = UnifiedJobEventPagination + resource_purpose = 'job events of a job' def get_queryset(self): job = self.get_parent_object() @@ -3589,7 +3885,9 @@ class JobJobEventsList(BaseJobEventsList): class JobJobEventsChildrenSummary(APIView): renderer_classes = [JSONRenderer] meta_events = ('debug', 'verbose', 'warning', 'error', 'system_warning', 'deprecated') + resource_purpose = 'children summary for job events' + @extend_schema_if_available(extensions={"x-ai-description": "Get children summary for job events"}) def get(self, request, **kwargs): resp = dict(children_summary={}, meta_event_nested_uuid={}, event_processing_finished=False, is_tree=True) job = get_object_or_404(models.Job, pk=kwargs['pk']) @@ -3695,6 +3993,7 @@ class AdHocCommandList(ListCreateAPIView): model = models.AdHocCommand serializer_class = serializers.AdHocCommandListSerializer always_allow_superuser = False + resource_purpose = 'ad hoc commands' @transaction.non_atomic_requests def dispatch(self, *args, **kwargs): @@ -3749,32 +4048,41 @@ class InventoryAdHocCommandsList(AdHocCommandList, SubListCreateAPIView): parent_model = models.Inventory relationship = 'ad_hoc_commands' parent_key = 'inventory' + resource_purpose = 'ad hoc command for an inventory' class GroupAdHocCommandsList(AdHocCommandList, SubListCreateAPIView): parent_model = models.Group relationship = 'ad_hoc_commands' + resource_purpose = 'ad hoc commands for a group' class HostAdHocCommandsList(AdHocCommandList, SubListCreateAPIView): parent_model = models.Host relationship = 'ad_hoc_commands' + resource_purpose = 'ad hoc commands of a host' class AdHocCommandDetail(UnifiedJobDeletionMixin, RetrieveDestroyAPIView): model = models.AdHocCommand serializer_class = serializers.AdHocCommandDetailSerializer + resource_purpose = 'ad hoc command detail' +@extend_schema_if_available( + extensions={'x-ai-description': 'Cancel an ad hoc command'}, +) class AdHocCommandCancel(GenericCancelView): model = models.AdHocCommand serializer_class = serializers.AdHocCommandCancelSerializer + resource_purpose = 'cancel for an ad hoc command' class AdHocCommandRelaunch(GenericAPIView): model = models.AdHocCommand obj_permission_type = 'start' serializer_class = serializers.AdHocCommandRelaunchSerializer + resource_purpose = 'relaunch an ad hoc command' # FIXME: Figure out why OPTIONS request still shows all fields. @@ -3782,11 +4090,17 @@ class AdHocCommandRelaunch(GenericAPIView): def dispatch(self, *args, **kwargs): return super(AdHocCommandRelaunch, self).dispatch(*args, **kwargs) + @extend_schema_if_available( + extensions={'x-ai-description': 'Return passwords needed to start an ad hoc command'}, + ) def get(self, request, *args, **kwargs): obj = self.get_object() data = dict(passwords_needed_to_start=obj.passwords_needed_to_start) return Response(data) + @extend_schema_if_available( + extensions={'x-ai-description': 'Relaunch an ad hoc command'}, + ) def post(self, request, *args, **kwargs): obj = self.get_object() @@ -3827,6 +4141,7 @@ class AdHocCommandRelaunch(GenericAPIView): class AdHocCommandEventDetail(RetrieveAPIView): model = models.AdHocCommandEvent serializer_class = serializers.AdHocCommandEventSerializer + resource_purpose = 'ad hoc command event detail' def get_serializer_context(self): context = super().get_serializer_context() @@ -3842,6 +4157,7 @@ class BaseAdHocCommandEventsList(NoTruncateMixin, SubListAPIView): name = _('Ad Hoc Command Events List') search_fields = ('stdout',) pagination_class = UnifiedJobEventPagination + resource_purpose = 'base view for ad hoc command events' def get_queryset(self): parent = self.get_parent_object() @@ -3851,6 +4167,7 @@ class BaseAdHocCommandEventsList(NoTruncateMixin, SubListAPIView): class HostAdHocCommandEventsList(BaseAdHocCommandEventsList): parent_model = models.Host + resource_purpose = 'events of ad hoc command of a host' def get_queryset(self): return super(BaseAdHocCommandEventsList, self).get_queryset() @@ -3862,6 +4179,7 @@ class HostAdHocCommandEventsList(BaseAdHocCommandEventsList): class AdHocCommandAdHocCommandEventsList(BaseAdHocCommandEventsList): parent_model = models.AdHocCommand + resource_purpose = 'events of an ad hoc command' class AdHocCommandActivityStreamList(SubListAPIView): @@ -3870,6 +4188,7 @@ class AdHocCommandActivityStreamList(SubListAPIView): parent_model = models.AdHocCommand relationship = 'activitystream_set' search_fields = ('changes',) + resource_purpose = 'activity stream of an ad hoc command' class AdHocCommandNotificationsList(SubListAPIView): @@ -3878,12 +4197,15 @@ class AdHocCommandNotificationsList(SubListAPIView): parent_model = models.AdHocCommand relationship = 'notifications' search_fields = ('subject', 'notification_type', 'body') + resource_purpose = 'notifications of an ad hoc command' class SystemJobList(ListAPIView): model = models.SystemJob serializer_class = serializers.SystemJobListSerializer + resource_purpose = 'system jobs' + @extend_schema_if_available(extensions={"x-ai-description": "List system jobs"}) def get(self, request, *args, **kwargs): if not request.user.is_superuser and not request.user.is_system_auditor: raise PermissionDenied(_("Superuser privileges needed.")) @@ -3893,11 +4215,13 @@ class SystemJobList(ListAPIView): class SystemJobDetail(UnifiedJobDeletionMixin, RetrieveDestroyAPIView): model = models.SystemJob serializer_class = serializers.SystemJobSerializer + resource_purpose = 'system job detail' class SystemJobCancel(GenericCancelView): model = models.SystemJob serializer_class = serializers.SystemJobCancelSerializer + resource_purpose = 'cancel for a system job' class SystemJobNotificationsList(SubListAPIView): @@ -3906,18 +4230,21 @@ class SystemJobNotificationsList(SubListAPIView): parent_model = models.SystemJob relationship = 'notifications' search_fields = ('subject', 'notification_type', 'body') + resource_purpose = 'notifications of a system job' class UnifiedJobTemplateList(ListAPIView): model = models.UnifiedJobTemplate serializer_class = serializers.UnifiedJobTemplateSerializer search_fields = ('description', 'name', 'jobtemplate__playbook') + resource_purpose = 'unified job templates' class UnifiedJobList(ListAPIView): model = models.UnifiedJob serializer_class = serializers.UnifiedJobListSerializer search_fields = ('description', 'name', 'job__playbook') + resource_purpose = 'unified jobs' def redact_ansi(line): @@ -3972,6 +4299,7 @@ class UnifiedJobStdout(RetrieveAPIView): renderers.AnsiDownloadRenderer, ] filter_backends = () + resource_purpose = 'stdout output of a unified job' def retrieve(self, request, *args, **kwargs): unified_job = self.get_object() @@ -4035,29 +4363,36 @@ class UnifiedJobStdout(RetrieveAPIView): class ProjectUpdateStdout(UnifiedJobStdout): model = models.ProjectUpdate + resource_purpose = 'stdout output of a project update' class InventoryUpdateStdout(UnifiedJobStdout): model = models.InventoryUpdate + resource_purpose = 'stdout output of an inventory update' class JobStdout(UnifiedJobStdout): model = models.Job + resource_purpose = 'stdout output of a job' class AdHocCommandStdout(UnifiedJobStdout): model = models.AdHocCommand + resource_purpose = 'stdout output of an ad hoc command' class NotificationTemplateList(ListCreateAPIView): model = models.NotificationTemplate serializer_class = serializers.NotificationTemplateSerializer + resource_purpose = 'notification templates' class NotificationTemplateDetail(RetrieveUpdateDestroyAPIView): model = models.NotificationTemplate serializer_class = serializers.NotificationTemplateSerializer + resource_purpose = 'notification template detail' + @extend_schema_if_available(extensions={"x-ai-description": "Delete a notification template"}) def delete(self, request, *args, **kwargs): obj = self.get_object() if not request.user.can_access(self.model, 'delete', obj): @@ -4076,7 +4411,9 @@ class NotificationTemplateTest(GenericAPIView): model = models.NotificationTemplate obj_permission_type = 'start' serializer_class = serializers.EmptySerializer + resource_purpose = 'test a notification template' + @extend_schema_if_available(extensions={"x-ai-description": "Send a test notification from a notification template"}) def post(self, request, *args, **kwargs): obj = self.get_object() msg = "Notification Test {} {}".format(obj.id, settings.TOWER_URL_BASE) @@ -4106,33 +4443,39 @@ class NotificationTemplateNotificationList(SubListAPIView): relationship = 'notifications' parent_key = 'notification_template' search_fields = ('subject', 'notification_type', 'body') + resource_purpose = 'notifications of a notification template' class NotificationTemplateCopy(CopyAPIView): model = models.NotificationTemplate copy_return_serializer_class = serializers.NotificationTemplateSerializer + resource_purpose = 'copy a notification template' class NotificationList(ListAPIView): model = models.Notification serializer_class = serializers.NotificationSerializer search_fields = ('subject', 'notification_type', 'body') + resource_purpose = 'notifications' class NotificationDetail(RetrieveAPIView): model = models.Notification serializer_class = serializers.NotificationSerializer + resource_purpose = 'notification detail' class ActivityStreamList(SimpleListAPIView): model = models.ActivityStream serializer_class = serializers.ActivityStreamSerializer search_fields = ('changes',) + resource_purpose = 'audit trail entries for tracking system changes' class ActivityStreamDetail(RetrieveAPIView): model = models.ActivityStream serializer_class = serializers.ActivityStreamSerializer + resource_purpose = 'activity stream entry detail' class RoleList(ListAPIView): @@ -4141,12 +4484,14 @@ class RoleList(ListAPIView): serializer_class = serializers.RoleSerializer permission_classes = (IsAuthenticated,) search_fields = ('role_field', 'content_type__model') + resource_purpose = 'roles' class RoleDetail(RetrieveAPIView): deprecated = True model = models.Role serializer_class = serializers.RoleSerializer + resource_purpose = 'role detail' class RoleUsersList(SubListAttachDetachAPIView): @@ -4156,12 +4501,14 @@ class RoleUsersList(SubListAttachDetachAPIView): parent_model = models.Role relationship = 'members' ordering = ('username',) + resource_purpose = 'users with a role' def get_queryset(self): role = self.get_parent_object() self.check_parent_access(role) return role.members.all() + @extend_schema_if_available(extensions={"x-ai-description": "Add a user to a role"}) def post(self, request, *args, **kwargs): # Forbid implicit user creation here sub_id = request.data.get('id', None) @@ -4192,12 +4539,14 @@ class RoleTeamsList(SubListAttachDetachAPIView): parent_model = models.Role relationship = 'member_role.parents' permission_classes = (IsAuthenticated,) + resource_purpose = 'teams with a role' def get_queryset(self): role = self.get_parent_object() self.check_parent_access(role) return models.Team.objects.filter(member_role__children=role) + @extend_schema_if_available(extensions={"x-ai-description": "Add a team to a role"}) def post(self, request, pk, *args, **kwargs): sub_id = request.data.get('id', None) if not sub_id: @@ -4261,6 +4610,7 @@ for attr, value in list(locals().items()): class WorkflowApprovalTemplateDetail(RelatedJobsPreventDeleteMixin, RetrieveUpdateDestroyAPIView): model = models.WorkflowApprovalTemplate serializer_class = serializers.WorkflowApprovalTemplateSerializer + resource_purpose = 'workflow approval template detail' class WorkflowApprovalTemplateJobsList(SubListAPIView): @@ -4269,12 +4619,15 @@ class WorkflowApprovalTemplateJobsList(SubListAPIView): parent_model = models.WorkflowApprovalTemplate relationship = 'approvals' parent_key = 'workflow_approval_template' + resource_purpose = 'workflow approvals of a workflow approval template' class WorkflowApprovalList(ListAPIView): model = models.WorkflowApproval serializer_class = serializers.WorkflowApprovalListSerializer + resource_purpose = 'workflow approvals' + @extend_schema_if_available(extensions={"x-ai-description": "List workflow approvals"}) def get(self, request, *args, **kwargs): return super(WorkflowApprovalList, self).get(request, *args, **kwargs) @@ -4282,13 +4635,16 @@ class WorkflowApprovalList(ListAPIView): class WorkflowApprovalDetail(UnifiedJobDeletionMixin, RetrieveDestroyAPIView): model = models.WorkflowApproval serializer_class = serializers.WorkflowApprovalSerializer + resource_purpose = 'workflow approval detail' class WorkflowApprovalApprove(RetrieveAPIView): model = models.WorkflowApproval serializer_class = serializers.WorkflowApprovalViewSerializer permission_classes = (WorkflowApprovalPermission,) + resource_purpose = 'approve a workflow approval' + @extend_schema_if_available(extensions={"x-ai-description": "Approve a workflow approval"}) def post(self, request, *args, **kwargs): obj = self.get_object() if not request.user.can_access(models.WorkflowApproval, 'approve_or_deny', obj): @@ -4303,7 +4659,9 @@ class WorkflowApprovalDeny(RetrieveAPIView): model = models.WorkflowApproval serializer_class = serializers.WorkflowApprovalViewSerializer permission_classes = (WorkflowApprovalPermission,) + resource_purpose = 'deny a workflow approval' + @extend_schema_if_available(extensions={"x-ai-description": "Deny a workflow approval"}) def post(self, request, *args, **kwargs): obj = self.get_object() if not request.user.can_access(models.WorkflowApproval, 'approve_or_deny', obj): diff --git a/awx/api/views/analytics.py b/awx/api/views/analytics.py index 09110c0957..8019f40e9e 100644 --- a/awx/api/views/analytics.py +++ b/awx/api/views/analytics.py @@ -15,6 +15,8 @@ from rest_framework import status from collections import OrderedDict +from ansible_base.lib.utils.schema import extend_schema_if_available + AUTOMATION_ANALYTICS_API_URL_PATH = "/api/tower-analytics/v1" AWX_ANALYTICS_API_PREFIX = 'analytics' @@ -38,6 +40,8 @@ class MissingSettings(Exception): class GetNotAllowedMixin(object): + skip_ai_description = True + def get(self, request, format=None): return Response(status=status.HTTP_405_METHOD_NOT_ALLOWED) @@ -46,7 +50,11 @@ class AnalyticsRootView(APIView): permission_classes = (AnalyticsPermission,) name = _('Automation Analytics') swagger_topic = 'Automation Analytics' + resource_purpose = 'automation analytics endpoints' + @extend_schema_if_available( + extensions={'x-ai-description': 'Retrieve list of available analytics endpoints'}, + ) def get(self, request, format=None): data = OrderedDict() data['authorized'] = reverse('api:analytics_authorized', request=request) @@ -99,6 +107,8 @@ class AnalyticsGenericView(APIView): return Response(response.json(), status=response.status_code) """ + resource_purpose = 'base view for analytics api proxy' + permission_classes = (AnalyticsPermission,) @staticmethod @@ -257,67 +267,91 @@ class AnalyticsGenericView(APIView): class AnalyticsGenericListView(AnalyticsGenericView): + resource_purpose = 'analytics api proxy list view' + + @extend_schema_if_available(extensions={"x-ai-description": "Get analytics data from Red Hat Insights"}) def get(self, request, format=None): return self._send_to_analytics(request, method="GET") + @extend_schema_if_available(extensions={"x-ai-description": "Post query to Red Hat Insights analytics"}) def post(self, request, format=None): return self._send_to_analytics(request, method="POST") + @extend_schema_if_available(extensions={"x-ai-description": "Get analytics endpoint options"}) def options(self, request, format=None): return self._send_to_analytics(request, method="OPTIONS") class AnalyticsGenericDetailView(AnalyticsGenericView): + resource_purpose = 'analytics api proxy detail view' + + @extend_schema_if_available(extensions={"x-ai-description": "Get specific analytics resource from Red Hat Insights"}) def get(self, request, slug, format=None): return self._send_to_analytics(request, method="GET") + @extend_schema_if_available(extensions={"x-ai-description": "Post query for specific analytics resource to Red Hat Insights"}) def post(self, request, slug, format=None): return self._send_to_analytics(request, method="POST") + @extend_schema_if_available(extensions={"x-ai-description": "Get options for specific analytics resource"}) def options(self, request, slug, format=None): return self._send_to_analytics(request, method="OPTIONS") +@extend_schema_if_available( + extensions={'x-ai-description': 'Check if the user has access to Red Hat Insights'}, +) class AnalyticsAuthorizedView(AnalyticsGenericListView): name = _("Authorized") + resource_purpose = 'red hat insights authorization status' class AnalyticsReportsList(GetNotAllowedMixin, AnalyticsGenericListView): name = _("Reports") swagger_topic = "Automation Analytics" + resource_purpose = 'automation analytics reports' class AnalyticsReportDetail(AnalyticsGenericDetailView): name = _("Report") + resource_purpose = 'automation analytics report detail' class AnalyticsReportOptionsList(AnalyticsGenericListView): name = _("Report Options") + resource_purpose = 'automation analytics report options' class AnalyticsAdoptionRateList(GetNotAllowedMixin, AnalyticsGenericListView): name = _("Adoption Rate") + resource_purpose = 'automation analytics adoption rate data' class AnalyticsEventExplorerList(GetNotAllowedMixin, AnalyticsGenericListView): name = _("Event Explorer") + resource_purpose = 'automation analytics event explorer data' class AnalyticsHostExplorerList(GetNotAllowedMixin, AnalyticsGenericListView): name = _("Host Explorer") + resource_purpose = 'automation analytics host explorer data' class AnalyticsJobExplorerList(GetNotAllowedMixin, AnalyticsGenericListView): name = _("Job Explorer") + resource_purpose = 'automation analytics job explorer data' class AnalyticsProbeTemplatesList(GetNotAllowedMixin, AnalyticsGenericListView): name = _("Probe Templates") + resource_purpose = 'automation analytics probe templates' class AnalyticsProbeTemplateForHostsList(GetNotAllowedMixin, AnalyticsGenericListView): name = _("Probe Template For Hosts") + resource_purpose = 'automation analytics probe templates for hosts' class AnalyticsRoiTemplatesList(GetNotAllowedMixin, AnalyticsGenericListView): name = _("ROI Templates") + resource_purpose = 'automation analytics roi templates' diff --git a/awx/api/views/bulk.py b/awx/api/views/bulk.py index a78dc43a37..a1c56e7dff 100644 --- a/awx/api/views/bulk.py +++ b/awx/api/views/bulk.py @@ -1,5 +1,7 @@ from collections import OrderedDict +from ansible_base.lib.utils.schema import extend_schema_if_available + from django.utils.translation import gettext_lazy as _ from rest_framework.permissions import IsAuthenticated @@ -30,6 +32,7 @@ class BulkView(APIView): ] allowed_methods = ['GET', 'OPTIONS'] + @extend_schema_if_available(extensions={"x-ai-description": "Retrieves a list of available bulk actions"}) def get(self, request, format=None): '''List top level resources''' data = OrderedDict() @@ -45,11 +48,13 @@ class BulkJobLaunchView(GenericAPIView): serializer_class = serializers.BulkJobLaunchSerializer allowed_methods = ['GET', 'POST', 'OPTIONS'] + @extend_schema_if_available(extensions={"x-ai-description": "Get information about bulk job launch endpoint"}) def get(self, request): data = OrderedDict() data['detail'] = "Specify a list of unified job templates to launch alongside their launchtime parameters" return Response(data, status=status.HTTP_200_OK) + @extend_schema_if_available(extensions={"x-ai-description": "Bulk launch job templates"}) def post(self, request): bulkjob_serializer = serializers.BulkJobLaunchSerializer(data=request.data, context={'request': request}) if bulkjob_serializer.is_valid(): @@ -64,9 +69,11 @@ class BulkHostCreateView(GenericAPIView): serializer_class = serializers.BulkHostCreateSerializer allowed_methods = ['GET', 'POST', 'OPTIONS'] + @extend_schema_if_available(extensions={"x-ai-description": "Get information about bulk host create endpoint"}) def get(self, request): return Response({"detail": "Bulk create hosts with this endpoint"}, status=status.HTTP_200_OK) + @extend_schema_if_available(extensions={"x-ai-description": "Bulk create hosts"}) def post(self, request): serializer = serializers.BulkHostCreateSerializer(data=request.data, context={'request': request}) if serializer.is_valid(): @@ -81,9 +88,11 @@ class BulkHostDeleteView(GenericAPIView): serializer_class = serializers.BulkHostDeleteSerializer allowed_methods = ['GET', 'POST', 'OPTIONS'] + @extend_schema_if_available(extensions={"x-ai-description": "Get information about bulk host delete endpoint"}) def get(self, request): return Response({"detail": "Bulk delete hosts with this endpoint"}, status=status.HTTP_200_OK) + @extend_schema_if_available(extensions={"x-ai-description": "Bulk delete hosts"}) def post(self, request): serializer = serializers.BulkHostDeleteSerializer(data=request.data, context={'request': request}) if serializer.is_valid(): diff --git a/awx/api/views/debug.py b/awx/api/views/debug.py index 8ccdd6afe8..b05fb94358 100644 --- a/awx/api/views/debug.py +++ b/awx/api/views/debug.py @@ -5,6 +5,7 @@ from django.conf import settings from rest_framework.permissions import AllowAny from rest_framework.response import Response from awx.api.generics import APIView +from ansible_base.lib.utils.schema import extend_schema_if_available from awx.main.scheduler import TaskManager, DependencyManager, WorkflowManager @@ -14,7 +15,9 @@ class TaskManagerDebugView(APIView): exclude_from_schema = True permission_classes = [AllowAny] prefix = 'Task' + resource_purpose = 'debug task manager' + @extend_schema_if_available(extensions={"x-ai-description": "Trigger task manager scheduling"}) def get(self, request): TaskManager().schedule() if not settings.AWX_DISABLE_TASK_MANAGERS: @@ -29,7 +32,9 @@ class DependencyManagerDebugView(APIView): exclude_from_schema = True permission_classes = [AllowAny] prefix = 'Dependency' + resource_purpose = 'debug dependency manager' + @extend_schema_if_available(extensions={"x-ai-description": "Trigger dependency manager scheduling"}) def get(self, request): DependencyManager().schedule() if not settings.AWX_DISABLE_TASK_MANAGERS: @@ -44,7 +49,9 @@ class WorkflowManagerDebugView(APIView): exclude_from_schema = True permission_classes = [AllowAny] prefix = 'Workflow' + resource_purpose = 'debug workflow manager' + @extend_schema_if_available(extensions={"x-ai-description": "Trigger workflow manager scheduling"}) def get(self, request): WorkflowManager().schedule() if not settings.AWX_DISABLE_TASK_MANAGERS: @@ -58,7 +65,9 @@ class DebugRootView(APIView): _ignore_model_permissions = True exclude_from_schema = True permission_classes = [AllowAny] + resource_purpose = 'debug endpoints root' + @extend_schema_if_available(extensions={"x-ai-description": "List available debug endpoints"}) def get(self, request, format=None): '''List of available debug urls''' data = OrderedDict() diff --git a/awx/api/views/instance_install_bundle.py b/awx/api/views/instance_install_bundle.py index e6a0fb98c8..d23a32efba 100644 --- a/awx/api/views/instance_install_bundle.py +++ b/awx/api/views/instance_install_bundle.py @@ -10,6 +10,7 @@ import time import re import asn1 +from ansible_base.lib.utils.schema import extend_schema_if_available from awx.api import serializers from awx.api.generics import GenericAPIView, Response from awx.api.permissions import IsSystemAdmin @@ -49,7 +50,9 @@ class InstanceInstallBundle(GenericAPIView): model = models.Instance serializer_class = serializers.InstanceSerializer permission_classes = (IsSystemAdmin,) + resource_purpose = 'install bundle' + @extend_schema_if_available(extensions={"x-ai-description": "Generate and download install bundle for an instance"}) def get(self, request, *args, **kwargs): instance_obj = self.get_object() diff --git a/awx/api/views/inventory.py b/awx/api/views/inventory.py index fb4f8e482e..b48cc87519 100644 --- a/awx/api/views/inventory.py +++ b/awx/api/views/inventory.py @@ -19,6 +19,8 @@ from rest_framework import serializers # AWX from awx.main.models import ActivityStream, Inventory, JobTemplate, Role, User, InstanceGroup, InventoryUpdateEvent, InventoryUpdate +from ansible_base.lib.utils.schema import extend_schema_if_available + from awx.api.generics import ( ListCreateAPIView, RetrieveUpdateDestroyAPIView, @@ -55,6 +57,7 @@ class InventoryUpdateEventsList(SubListAPIView): name = _('Inventory Update Events List') search_fields = ('stdout',) pagination_class = UnifiedJobEventPagination + resource_purpose = 'events of an inventory update' def get_queryset(self): iu = self.get_parent_object() @@ -69,11 +72,13 @@ class InventoryUpdateEventsList(SubListAPIView): class InventoryList(ListCreateAPIView): model = Inventory serializer_class = InventorySerializer + resource_purpose = 'inventories' class InventoryDetail(RelatedJobsPreventDeleteMixin, RetrieveUpdateDestroyAPIView): model = Inventory serializer_class = InventorySerializer + resource_purpose = 'inventory detail' def update(self, request, *args, **kwargs): obj = self.get_object() @@ -100,33 +105,39 @@ class InventoryDetail(RelatedJobsPreventDeleteMixin, RetrieveUpdateDestroyAPIVie class ConstructedInventoryDetail(InventoryDetail): serializer_class = ConstructedInventorySerializer + resource_purpose = 'constructed inventory detail' class ConstructedInventoryList(InventoryList): serializer_class = ConstructedInventorySerializer + resource_purpose = 'constructed inventories' def get_queryset(self): r = super().get_queryset() return r.filter(kind='constructed') +@extend_schema_if_available(extensions={"x-ai-description": "Get or create input inventory inventory"}) class InventoryInputInventoriesList(SubListAttachDetachAPIView): model = Inventory serializer_class = InventorySerializer parent_model = Inventory relationship = 'input_inventories' + resource_purpose = 'input inventories of a constructed inventory' def is_valid_relation(self, parent, sub, created=False): if sub.kind == 'constructed': raise serializers.ValidationError({'error': 'You cannot add a constructed inventory to another constructed inventory.'}) +@extend_schema_if_available(extensions={"x-ai-description": "Get activity stream for an inventory"}) class InventoryActivityStreamList(SubListAPIView): model = ActivityStream serializer_class = ActivityStreamSerializer parent_model = Inventory relationship = 'activitystream_set' search_fields = ('changes',) + resource_purpose = 'activity stream for an inventory' def get_queryset(self): parent = self.get_parent_object() @@ -140,11 +151,13 @@ class InventoryInstanceGroupsList(SubListAttachDetachAPIView): serializer_class = InstanceGroupSerializer parent_model = Inventory relationship = 'instance_groups' + resource_purpose = 'instance groups of an inventory' class InventoryAccessList(ResourceAccessList): model = User # needs to be User for AccessLists's parent_model = Inventory + resource_purpose = 'users who can access the inventory' class InventoryObjectRolesList(SubListAPIView): @@ -153,6 +166,7 @@ class InventoryObjectRolesList(SubListAPIView): parent_model = Inventory search_fields = ('role_field', 'content_type__model') deprecated = True + resource_purpose = 'roles of an inventory' def get_queryset(self): po = self.get_parent_object() @@ -165,6 +179,7 @@ class InventoryJobTemplateList(SubListAPIView): serializer_class = JobTemplateSerializer parent_model = Inventory relationship = 'jobtemplates' + resource_purpose = 'job templates using an inventory' def get_queryset(self): parent = self.get_parent_object() @@ -175,8 +190,10 @@ class InventoryJobTemplateList(SubListAPIView): class InventoryLabelList(LabelSubListCreateAttachDetachView): parent_model = Inventory + resource_purpose = 'labels of an inventory' class InventoryCopy(CopyAPIView): model = Inventory copy_return_serializer_class = InventorySerializer + resource_purpose = 'copy of an inventory' diff --git a/awx/api/views/labels.py b/awx/api/views/labels.py index a1099143da..3bc46d5203 100644 --- a/awx/api/views/labels.py +++ b/awx/api/views/labels.py @@ -2,6 +2,7 @@ from awx.api.generics import SubListCreateAttachDetachAPIView, RetrieveUpdateAPIView, ListCreateAPIView from awx.main.models import Label from awx.api.serializers import LabelSerializer +from ansible_base.lib.utils.schema import extend_schema_if_available # Django from django.utils.translation import gettext_lazy as _ @@ -24,6 +25,7 @@ class LabelSubListCreateAttachDetachView(SubListCreateAttachDetachAPIView): model = Label serializer_class = LabelSerializer relationship = 'labels' + resource_purpose = 'labels of a resource' def unattach(self, request, *args, **kwargs): (sub_id, res) = super().unattach_validate(request) @@ -39,6 +41,7 @@ class LabelSubListCreateAttachDetachView(SubListCreateAttachDetachAPIView): return res + @extend_schema_if_available(extensions={"x-ai-description": "Create or attach a label to a resource"}) def post(self, request, *args, **kwargs): # If a label already exists in the database, attach it instead of erroring out # that it already exists @@ -61,9 +64,11 @@ class LabelSubListCreateAttachDetachView(SubListCreateAttachDetachAPIView): class LabelDetail(RetrieveUpdateAPIView): model = Label serializer_class = LabelSerializer + resource_purpose = 'label detail' class LabelList(ListCreateAPIView): name = _("Labels") model = Label serializer_class = LabelSerializer + resource_purpose = 'labels' diff --git a/awx/api/views/mesh_visualizer.py b/awx/api/views/mesh_visualizer.py index e768989729..714c580f16 100644 --- a/awx/api/views/mesh_visualizer.py +++ b/awx/api/views/mesh_visualizer.py @@ -2,6 +2,7 @@ # All Rights Reserved. from django.utils.translation import gettext_lazy as _ +from ansible_base.lib.utils.schema import extend_schema_if_available from awx.api.generics import APIView, Response from awx.api.permissions import IsSystemAdminOrAuditor @@ -13,7 +14,9 @@ class MeshVisualizer(APIView): name = _("Mesh Visualizer") permission_classes = (IsSystemAdminOrAuditor,) swagger_topic = "System Configuration" + resource_purpose = 'mesh network topology visualization data' + @extend_schema_if_available(extensions={"x-ai-description": "Get mesh network topology visualization data"}) def get(self, request, format=None): data = { 'nodes': InstanceNodeSerializer(Instance.objects.all(), many=True).data, diff --git a/awx/api/views/metrics.py b/awx/api/views/metrics.py index a5a628f510..2cda92cdd8 100644 --- a/awx/api/views/metrics.py +++ b/awx/api/views/metrics.py @@ -7,6 +7,7 @@ import logging # Django from django.conf import settings from django.utils.translation import gettext_lazy as _ +from ansible_base.lib.utils.schema import extend_schema_if_available # Django REST Framework from rest_framework.permissions import AllowAny @@ -29,6 +30,7 @@ logger = logging.getLogger('awx.analytics') class MetricsView(APIView): name = _('Metrics') swagger_topic = 'Metrics' + resource_purpose = 'prometheus metrics data' renderer_classes = [renderers.PlainTextRenderer, renderers.PrometheusJSONRenderer, renderers.BrowsableAPIRenderer] @@ -37,6 +39,7 @@ class MetricsView(APIView): self.permission_classes = (AllowAny,) return super(APIView, self).initialize_request(request, *args, **kwargs) + @extend_schema_if_available(extensions={"x-ai-description": "Get Prometheus metrics data"}) def get(self, request): '''Show Metrics Details''' if settings.ALLOW_METRICS_FOR_ANONYMOUS_USERS or request.user.is_superuser or request.user.is_system_auditor: diff --git a/awx/api/views/organization.py b/awx/api/views/organization.py index 021f90a738..8795c2ed12 100644 --- a/awx/api/views/organization.py +++ b/awx/api/views/organization.py @@ -60,11 +60,13 @@ logger = logging.getLogger('awx.api.views.organization') class OrganizationList(OrganizationCountsMixin, ListCreateAPIView): model = Organization serializer_class = OrganizationSerializer + resource_purpose = 'organizations' class OrganizationDetail(RelatedJobsPreventDeleteMixin, RetrieveUpdateDestroyAPIView): model = Organization serializer_class = OrganizationSerializer + resource_purpose = 'organization detail' def get_serializer_context(self, *args, **kwargs): full_context = super(OrganizationDetail, self).get_serializer_context(*args, **kwargs) @@ -102,6 +104,7 @@ class OrganizationInventoriesList(SubListAPIView): serializer_class = InventorySerializer parent_model = Organization relationship = 'inventories' + resource_purpose = 'inventories of an organization' class OrganizationUsersList(BaseUsersList): @@ -110,6 +113,7 @@ class OrganizationUsersList(BaseUsersList): parent_model = Organization relationship = 'member_role.members' ordering = ('username',) + resource_purpose = 'users of an organization' class OrganizationAdminsList(BaseUsersList): @@ -118,6 +122,7 @@ class OrganizationAdminsList(BaseUsersList): parent_model = Organization relationship = 'admin_role.members' ordering = ('username',) + resource_purpose = 'administrators of an organization' class OrganizationProjectsList(SubListCreateAPIView): @@ -125,6 +130,7 @@ class OrganizationProjectsList(SubListCreateAPIView): serializer_class = ProjectSerializer parent_model = Organization parent_key = 'organization' + resource_purpose = 'projects of an organization' class OrganizationExecutionEnvironmentsList(SubListCreateAttachDetachAPIView): @@ -134,6 +140,7 @@ class OrganizationExecutionEnvironmentsList(SubListCreateAttachDetachAPIView): relationship = 'executionenvironments' parent_key = 'organization' swagger_topic = "Execution Environments" + resource_purpose = 'execution environments of an organization' class OrganizationJobTemplatesList(SubListCreateAPIView): @@ -141,6 +148,7 @@ class OrganizationJobTemplatesList(SubListCreateAPIView): serializer_class = JobTemplateSerializer parent_model = Organization parent_key = 'organization' + resource_purpose = 'job templates of an organization' class OrganizationWorkflowJobTemplatesList(SubListCreateAPIView): @@ -148,6 +156,7 @@ class OrganizationWorkflowJobTemplatesList(SubListCreateAPIView): serializer_class = WorkflowJobTemplateSerializer parent_model = Organization parent_key = 'organization' + resource_purpose = 'workflow job templates of an organization' class OrganizationTeamsList(SubListCreateAttachDetachAPIView): @@ -156,6 +165,7 @@ class OrganizationTeamsList(SubListCreateAttachDetachAPIView): parent_model = Organization relationship = 'teams' parent_key = 'organization' + resource_purpose = 'teams of an organization' class OrganizationActivityStreamList(SubListAPIView): @@ -164,6 +174,7 @@ class OrganizationActivityStreamList(SubListAPIView): parent_model = Organization relationship = 'activitystream_set' search_fields = ('changes',) + resource_purpose = 'activity stream for an organization' class OrganizationNotificationTemplatesList(SubListCreateAttachDetachAPIView): @@ -172,28 +183,34 @@ class OrganizationNotificationTemplatesList(SubListCreateAttachDetachAPIView): parent_model = Organization relationship = 'notification_templates' parent_key = 'organization' + resource_purpose = 'notification templates of an organization' class OrganizationNotificationTemplatesAnyList(SubListCreateAttachDetachAPIView): model = NotificationTemplate serializer_class = NotificationTemplateSerializer parent_model = Organization + resource_purpose = 'base view for notification templates of an organization' class OrganizationNotificationTemplatesStartedList(OrganizationNotificationTemplatesAnyList): relationship = 'notification_templates_started' + resource_purpose = 'notification templates for job started events of an organization' class OrganizationNotificationTemplatesErrorList(OrganizationNotificationTemplatesAnyList): relationship = 'notification_templates_error' + resource_purpose = 'notification templates for job error events of an organization' class OrganizationNotificationTemplatesSuccessList(OrganizationNotificationTemplatesAnyList): relationship = 'notification_templates_success' + resource_purpose = 'notification templates for job success events of an organization' class OrganizationNotificationTemplatesApprovalList(OrganizationNotificationTemplatesAnyList): relationship = 'notification_templates_approvals' + resource_purpose = 'notification templates for workflow approval events of an organization' class OrganizationInstanceGroupsList(OrganizationInstanceGroupMembershipMixin, SubListAttachDetachAPIView): @@ -202,6 +219,7 @@ class OrganizationInstanceGroupsList(OrganizationInstanceGroupMembershipMixin, S parent_model = Organization relationship = 'instance_groups' filter_read_permission = False + resource_purpose = 'instance groups of an organization' class OrganizationGalaxyCredentialsList(SubListAttachDetachAPIView): @@ -210,6 +228,7 @@ class OrganizationGalaxyCredentialsList(SubListAttachDetachAPIView): parent_model = Organization relationship = 'galaxy_credentials' filter_read_permission = False + resource_purpose = 'galaxy credentials of an organization' def is_valid_relation(self, parent, sub, created=False): if sub.kind != 'galaxy_api_token': @@ -219,6 +238,7 @@ class OrganizationGalaxyCredentialsList(SubListAttachDetachAPIView): class OrganizationAccessList(ResourceAccessList): model = User # needs to be User for AccessLists's parent_model = Organization + resource_purpose = 'users who can access the organization' class OrganizationObjectRolesList(SubListAPIView): @@ -227,6 +247,7 @@ class OrganizationObjectRolesList(SubListAPIView): parent_model = Organization search_fields = ('role_field', 'content_type__model') deprecated = True + resource_purpose = 'roles of an organization' def get_queryset(self): po = self.get_parent_object() diff --git a/awx/api/views/root.py b/awx/api/views/root.py index 0e369a1e97..f05e36d70b 100644 --- a/awx/api/views/root.py +++ b/awx/api/views/root.py @@ -23,6 +23,8 @@ from rest_framework import status import requests +from ansible_base.lib.utils.schema import extend_schema_if_available + from awx import MODE from awx.api.generics import APIView from awx.conf.registry import settings_registry @@ -46,8 +48,10 @@ class ApiRootView(APIView): name = _('REST API') versioning_class = URLPathVersioning swagger_topic = 'Versioning' + resource_purpose = 'api root and version information' @method_decorator(ensure_csrf_cookie) + @extend_schema_if_available(extensions={"x-ai-description": "List supported API versions"}) def get(self, request, format=None): '''List supported API versions''' v2 = reverse('api:api_v2_root_view', request=request, kwargs={'version': 'v2'}) @@ -66,7 +70,9 @@ class ApiRootView(APIView): class ApiVersionRootView(APIView): permission_classes = (AllowAny,) swagger_topic = 'Versioning' + resource_purpose = 'api top-level resources' + @extend_schema_if_available(extensions={"x-ai-description": "List top-level API resources"}) def get(self, request, format=None): '''List top level resources''' data = OrderedDict() @@ -126,6 +132,7 @@ class ApiVersionRootView(APIView): class ApiV2RootView(ApiVersionRootView): name = _('Version 2') + resource_purpose = 'api v2 root' class ApiV2PingView(APIView): @@ -137,7 +144,11 @@ class ApiV2PingView(APIView): authentication_classes = () name = _('Ping') swagger_topic = 'System Configuration' + resource_purpose = 'basic instance information' + @extend_schema_if_available( + extensions={'x-ai-description': 'Return basic information about this instance'}, + ) def get(self, request, format=None): """Return some basic information about this instance @@ -172,12 +183,16 @@ class ApiV2SubscriptionView(APIView): permission_classes = (IsAuthenticated,) name = _('Subscriptions') swagger_topic = 'System Configuration' + resource_purpose = 'aap subscription validation' def check_permissions(self, request): super(ApiV2SubscriptionView, self).check_permissions(request) if not request.user.is_superuser and request.method.lower() not in {'options', 'head'}: self.permission_denied(request) # Raises PermissionDenied exception. + @extend_schema_if_available( + extensions={'x-ai-description': 'List valid AAP subscriptions'}, + ) def post(self, request): data = request.data.copy() @@ -244,12 +259,16 @@ class ApiV2AttachView(APIView): permission_classes = (IsAuthenticated,) name = _('Attach Subscription') swagger_topic = 'System Configuration' + resource_purpose = 'subscription attachment' def check_permissions(self, request): super(ApiV2AttachView, self).check_permissions(request) if not request.user.is_superuser and request.method.lower() not in {'options', 'head'}: self.permission_denied(request) # Raises PermissionDenied exception. + @extend_schema_if_available( + extensions={'x-ai-description': 'Attach a subscription'}, + ) def post(self, request): data = request.data.copy() subscription_id = data.get('subscription_id', None) @@ -299,12 +318,16 @@ class ApiV2ConfigView(APIView): permission_classes = (IsAuthenticated,) name = _('Configuration') swagger_topic = 'System Configuration' + resource_purpose = 'system configuration and license management' def check_permissions(self, request): super(ApiV2ConfigView, self).check_permissions(request) if not request.user.is_superuser and request.method.lower() not in {'options', 'head', 'get'}: self.permission_denied(request) # Raises PermissionDenied exception. + @extend_schema_if_available( + extensions={'x-ai-description': 'Return various configuration settings'}, + ) def get(self, request, format=None): '''Return various sitewide configuration settings''' @@ -343,6 +366,9 @@ class ApiV2ConfigView(APIView): return Response(data) + @extend_schema_if_available( + extensions={'x-ai-description': 'Upload a subscription manifest'}, + ) def post(self, request): if not isinstance(request.data, dict): return Response({"error": _("Invalid subscription data")}, status=status.HTTP_400_BAD_REQUEST) @@ -388,6 +414,9 @@ class ApiV2ConfigView(APIView): logger.warning(smart_str(u"Invalid subscription submitted."), extra=dict(actor=request.user.username)) return Response({"error": _("Invalid subscription")}, status=status.HTTP_400_BAD_REQUEST) + @extend_schema_if_available( + extensions={'x-ai-description': 'Remove the current subscription'}, + ) def delete(self, request): try: settings.LICENSE = {} diff --git a/awx/api/views/webhooks.py b/awx/api/views/webhooks.py index c0fa81380e..fab897eebf 100644 --- a/awx/api/views/webhooks.py +++ b/awx/api/views/webhooks.py @@ -11,6 +11,7 @@ from rest_framework import status from rest_framework.exceptions import PermissionDenied from rest_framework.permissions import AllowAny from rest_framework.response import Response +from ansible_base.lib.utils.schema import extend_schema_if_available from awx.api import serializers from awx.api.generics import APIView, GenericAPIView @@ -24,6 +25,7 @@ logger = logging.getLogger('awx.api.views.webhooks') class WebhookKeyView(GenericAPIView): serializer_class = serializers.EmptySerializer permission_classes = (WebhookKeyPermission,) + resource_purpose = 'webhook key management' def get_queryset(self): qs_models = {'job_templates': JobTemplate, 'workflow_job_templates': WorkflowJobTemplate} @@ -31,11 +33,13 @@ class WebhookKeyView(GenericAPIView): return super().get_queryset() + @extend_schema_if_available(extensions={"x-ai-description": "Get the webhook key for a template"}) def get(self, request, *args, **kwargs): obj = self.get_object() return Response({'webhook_key': obj.webhook_key}) + @extend_schema_if_available(extensions={"x-ai-description": "Rotate the webhook key for a template"}) def post(self, request, *args, **kwargs): obj = self.get_object() obj.rotate_webhook_key() @@ -52,6 +56,7 @@ class WebhookReceiverBase(APIView): authentication_classes = () ref_keys = {} + resource_purpose = 'webhook receiver for triggering jobs' def get_queryset(self): qs_models = {'job_templates': JobTemplate, 'workflow_job_templates': WorkflowJobTemplate} @@ -127,6 +132,7 @@ class WebhookReceiverBase(APIView): raise PermissionDenied @csrf_exempt + @extend_schema_if_available(extensions={"x-ai-description": "Receive a webhook event and trigger a job"}) def post(self, request, *args, **kwargs): # Ensure that the full contents of the request are captured for multiple uses. request.body @@ -175,6 +181,7 @@ class WebhookReceiverBase(APIView): class GithubWebhookReceiver(WebhookReceiverBase): service = 'github' + resource_purpose = 'github webhook receiver' ref_keys = { 'pull_request': 'pull_request.head.sha', @@ -212,6 +219,7 @@ class GithubWebhookReceiver(WebhookReceiverBase): class GitlabWebhookReceiver(WebhookReceiverBase): service = 'gitlab' + resource_purpose = 'gitlab webhook receiver' ref_keys = {'Push Hook': 'checkout_sha', 'Tag Push Hook': 'checkout_sha', 'Merge Request Hook': 'object_attributes.last_commit.id'} @@ -250,6 +258,7 @@ class GitlabWebhookReceiver(WebhookReceiverBase): class BitbucketDcWebhookReceiver(WebhookReceiverBase): service = 'bitbucket_dc' + resource_purpose = 'bitbucket data center webhook receiver' ref_keys = { 'repo:refs_changed': 'changes.0.toHash', diff --git a/awx/settings/defaults.py b/awx/settings/defaults.py index e05d654074..48754f7ae6 100644 --- a/awx/settings/defaults.py +++ b/awx/settings/defaults.py @@ -352,6 +352,7 @@ INSTALLED_APPS = [ 'ansible_base.resource_registry', 'ansible_base.rbac', 'ansible_base.feature_flags', + 'ansible_base.api_documentation', 'flags', ]