4544 lines
170 KiB
Python
4544 lines
170 KiB
Python
# Copyright (c) 2015 Ansible, Inc.
|
|
# All Rights Reserved.
|
|
|
|
# Python
|
|
import dateutil
|
|
import functools
|
|
import html
|
|
import logging
|
|
import re
|
|
import requests
|
|
import socket
|
|
import sys
|
|
import time
|
|
from base64 import b64encode
|
|
from collections import OrderedDict
|
|
|
|
from urllib3.exceptions import ConnectTimeoutError
|
|
|
|
|
|
# Django
|
|
from django.conf import settings
|
|
from django.core.exceptions import FieldError, ObjectDoesNotExist
|
|
from django.db.models import Q, Sum
|
|
from django.db import IntegrityError, transaction, connection
|
|
from django.shortcuts import get_object_or_404
|
|
from django.utils.safestring import mark_safe
|
|
from django.utils.timezone import now
|
|
from django.views.decorators.csrf import csrf_exempt
|
|
from django.template.loader import render_to_string
|
|
from django.http import HttpResponse
|
|
from django.contrib.contenttypes.models import ContentType
|
|
from django.utils.translation import ugettext_lazy as _
|
|
|
|
|
|
# Django REST Framework
|
|
from rest_framework.exceptions import APIException, PermissionDenied, ParseError, NotFound
|
|
from rest_framework.parsers import FormParser
|
|
from rest_framework.permissions import AllowAny, IsAuthenticated
|
|
from rest_framework.renderers import JSONRenderer, StaticHTMLRenderer
|
|
from rest_framework.response import Response
|
|
from rest_framework.settings import api_settings
|
|
from rest_framework.views import exception_handler, get_view_name
|
|
from rest_framework import status
|
|
|
|
# Django REST Framework YAML
|
|
from rest_framework_yaml.parsers import YAMLParser
|
|
from rest_framework_yaml.renderers import YAMLRenderer
|
|
|
|
# QSStats
|
|
import qsstats
|
|
|
|
# ANSIConv
|
|
import ansiconv
|
|
|
|
# Python Social Auth
|
|
from social_core.backends.utils import load_backends
|
|
|
|
# Django OAuth Toolkit
|
|
from oauth2_provider.models import get_access_token_model
|
|
|
|
import pytz
|
|
from wsgiref.util import FileWrapper
|
|
|
|
# AWX
|
|
from awx.main.tasks import send_notifications, update_inventory_computed_fields
|
|
from awx.main.access import get_user_queryset, HostAccess
|
|
from awx.api.generics import (
|
|
APIView, BaseUsersList, CopyAPIView, DeleteLastUnattachLabelMixin,
|
|
GenericAPIView, ListAPIView, ListCreateAPIView,
|
|
ResourceAccessList, RetrieveAPIView, RetrieveDestroyAPIView,
|
|
RetrieveUpdateAPIView, RetrieveUpdateDestroyAPIView, SimpleListAPIView,
|
|
SubDetailAPIView, SubListAPIView, SubListAttachDetachAPIView,
|
|
SubListCreateAPIView, SubListCreateAttachDetachAPIView,
|
|
SubListDestroyAPIView
|
|
)
|
|
from awx.api.versioning import reverse
|
|
from awx.main import models
|
|
from awx.main.utils import (
|
|
camelcase_to_underscore,
|
|
extract_ansible_vars,
|
|
get_awx_http_client_headers,
|
|
get_object_or_400,
|
|
getattrd,
|
|
get_pk_from_dict,
|
|
schedule_task_manager,
|
|
ignore_inventory_computed_fields,
|
|
set_environ
|
|
)
|
|
from awx.main.utils.encryption import encrypt_value
|
|
from awx.main.utils.filters import SmartFilter
|
|
from awx.main.utils.insights import filter_insights_api_response
|
|
from awx.main.redact import UriCleaner
|
|
from awx.api.permissions import (
|
|
JobTemplateCallbackPermission, TaskPermission, ProjectUpdatePermission,
|
|
InventoryInventorySourcesUpdatePermission, UserPermission,
|
|
InstanceGroupTowerPermission, VariableDataPermission,
|
|
WorkflowApprovalPermission
|
|
)
|
|
from awx.api import renderers
|
|
from awx.api import serializers
|
|
from awx.api.metadata import RoleMetadata
|
|
from awx.main.constants import ACTIVE_STATES
|
|
from awx.main.scheduler.dag_workflow import WorkflowDAG
|
|
from awx.api.views.mixin import (
|
|
ControlledByScmMixin, InstanceGroupMembershipMixin,
|
|
OrganizationCountsMixin, RelatedJobsPreventDeleteMixin,
|
|
UnifiedJobDeletionMixin, NoTruncateMixin,
|
|
)
|
|
from awx.api.views.organization import ( # noqa
|
|
OrganizationList,
|
|
OrganizationDetail,
|
|
OrganizationInventoriesList,
|
|
OrganizationUsersList,
|
|
OrganizationAdminsList,
|
|
OrganizationProjectsList,
|
|
OrganizationJobTemplatesList,
|
|
OrganizationWorkflowJobTemplatesList,
|
|
OrganizationTeamsList,
|
|
OrganizationActivityStreamList,
|
|
OrganizationNotificationTemplatesList,
|
|
OrganizationNotificationTemplatesAnyList,
|
|
OrganizationNotificationTemplatesErrorList,
|
|
OrganizationNotificationTemplatesStartedList,
|
|
OrganizationNotificationTemplatesSuccessList,
|
|
OrganizationNotificationTemplatesApprovalList,
|
|
OrganizationInstanceGroupsList,
|
|
OrganizationGalaxyCredentialsList,
|
|
OrganizationAccessList,
|
|
OrganizationObjectRolesList,
|
|
)
|
|
from awx.api.views.inventory import ( # noqa
|
|
InventoryList,
|
|
InventoryDetail,
|
|
InventoryUpdateEventsList,
|
|
InventoryScriptList,
|
|
InventoryScriptDetail,
|
|
InventoryScriptObjectRolesList,
|
|
InventoryScriptCopy,
|
|
InventoryList,
|
|
InventoryDetail,
|
|
InventoryActivityStreamList,
|
|
InventoryInstanceGroupsList,
|
|
InventoryAccessList,
|
|
InventoryObjectRolesList,
|
|
InventoryJobTemplateList,
|
|
InventoryCopy,
|
|
)
|
|
from awx.api.views.root import ( # noqa
|
|
ApiRootView,
|
|
ApiOAuthAuthorizationRootView,
|
|
ApiVersionRootView,
|
|
ApiV2RootView,
|
|
ApiV2PingView,
|
|
ApiV2ConfigView,
|
|
ApiV2SubscriptionView,
|
|
ApiV2AttachView,
|
|
)
|
|
from awx.api.views.webhooks import ( # noqa
|
|
WebhookKeyView,
|
|
GithubWebhookReceiver,
|
|
GitlabWebhookReceiver,
|
|
)
|
|
|
|
|
|
logger = logging.getLogger('awx.api.views')
|
|
|
|
|
|
def api_exception_handler(exc, context):
|
|
'''
|
|
Override default API exception handler to catch IntegrityError exceptions.
|
|
'''
|
|
if isinstance(exc, IntegrityError):
|
|
exc = ParseError(exc.args[0])
|
|
if isinstance(exc, FieldError):
|
|
exc = ParseError(exc.args[0])
|
|
if isinstance(context['view'], UnifiedJobStdout):
|
|
context['view'].renderer_classes = [renderers.BrowsableAPIRenderer, JSONRenderer]
|
|
if isinstance(exc, APIException):
|
|
req = context['request']._request
|
|
if 'awx.named_url_rewritten' in req.environ and not str(getattr(exc, 'status_code', 0)).startswith('2'):
|
|
# if the URL was rewritten, and it's not a 2xx level status code,
|
|
# revert the request.path to its original value to avoid leaking
|
|
# any context about the existance of resources
|
|
req.path = req.environ['awx.named_url_rewritten']
|
|
if exc.status_code == 403:
|
|
exc = NotFound(detail=_('Not found.'))
|
|
return exception_handler(exc, context)
|
|
|
|
|
|
class DashboardView(APIView):
|
|
|
|
deprecated = True
|
|
|
|
name = _("Dashboard")
|
|
swagger_topic = 'Dashboard'
|
|
|
|
def get(self, request, format=None):
|
|
''' Show Dashboard Details '''
|
|
data = OrderedDict()
|
|
data['related'] = {'jobs_graph': reverse('api:dashboard_jobs_graph_view', request=request)}
|
|
user_inventory = get_user_queryset(request.user, models.Inventory)
|
|
inventory_with_failed_hosts = user_inventory.filter(hosts_with_active_failures__gt=0)
|
|
user_inventory_external = user_inventory.filter(has_inventory_sources=True)
|
|
# if there are *zero* inventories, this aggregrate query will be None, fall back to 0
|
|
failed_inventory = user_inventory.aggregate(Sum('inventory_sources_with_failures'))['inventory_sources_with_failures__sum'] or 0
|
|
data['inventories'] = {'url': reverse('api:inventory_list', request=request),
|
|
'total': user_inventory.count(),
|
|
'total_with_inventory_source': user_inventory_external.count(),
|
|
'job_failed': inventory_with_failed_hosts.count(),
|
|
'inventory_failed': failed_inventory}
|
|
user_inventory_sources = get_user_queryset(request.user, models.InventorySource)
|
|
ec2_inventory_sources = user_inventory_sources.filter(source='ec2')
|
|
ec2_inventory_failed = ec2_inventory_sources.filter(status='failed')
|
|
data['inventory_sources'] = {}
|
|
data['inventory_sources']['ec2'] = {'url': reverse('api:inventory_source_list', request=request) + "?source=ec2",
|
|
'failures_url': reverse('api:inventory_source_list', request=request) + "?source=ec2&status=failed",
|
|
'label': 'Amazon EC2',
|
|
'total': ec2_inventory_sources.count(),
|
|
'failed': ec2_inventory_failed.count()}
|
|
|
|
user_groups = get_user_queryset(request.user, models.Group)
|
|
groups_inventory_failed = models.Group.objects.filter(inventory_sources__last_job_failed=True).count()
|
|
data['groups'] = {'url': reverse('api:group_list', request=request),
|
|
'total': user_groups.count(),
|
|
'inventory_failed': groups_inventory_failed}
|
|
|
|
user_hosts = get_user_queryset(request.user, models.Host)
|
|
user_hosts_failed = user_hosts.filter(last_job_host_summary__failed=True)
|
|
data['hosts'] = {'url': reverse('api:host_list', request=request),
|
|
'failures_url': reverse('api:host_list', request=request) + "?last_job_host_summary__failed=True",
|
|
'total': user_hosts.count(),
|
|
'failed': user_hosts_failed.count()}
|
|
|
|
user_projects = get_user_queryset(request.user, models.Project)
|
|
user_projects_failed = user_projects.filter(last_job_failed=True)
|
|
data['projects'] = {'url': reverse('api:project_list', request=request),
|
|
'failures_url': reverse('api:project_list', request=request) + "?last_job_failed=True",
|
|
'total': user_projects.count(),
|
|
'failed': user_projects_failed.count()}
|
|
|
|
git_projects = user_projects.filter(scm_type='git')
|
|
git_failed_projects = git_projects.filter(last_job_failed=True)
|
|
svn_projects = user_projects.filter(scm_type='svn')
|
|
svn_failed_projects = svn_projects.filter(last_job_failed=True)
|
|
archive_projects = user_projects.filter(scm_type='archive')
|
|
archive_failed_projects = archive_projects.filter(last_job_failed=True)
|
|
data['scm_types'] = {}
|
|
data['scm_types']['git'] = {'url': reverse('api:project_list', request=request) + "?scm_type=git",
|
|
'label': 'Git',
|
|
'failures_url': reverse('api:project_list', request=request) + "?scm_type=git&last_job_failed=True",
|
|
'total': git_projects.count(),
|
|
'failed': git_failed_projects.count()}
|
|
data['scm_types']['svn'] = {'url': reverse('api:project_list', request=request) + "?scm_type=svn",
|
|
'label': 'Subversion',
|
|
'failures_url': reverse('api:project_list', request=request) + "?scm_type=svn&last_job_failed=True",
|
|
'total': svn_projects.count(),
|
|
'failed': svn_failed_projects.count()}
|
|
data['scm_types']['archive'] = {'url': reverse('api:project_list', request=request) + "?scm_type=archive",
|
|
'label': 'Remote Archive',
|
|
'failures_url': reverse('api:project_list', request=request) + "?scm_type=archive&last_job_failed=True",
|
|
'total': archive_projects.count(),
|
|
'failed': archive_failed_projects.count()}
|
|
|
|
user_list = get_user_queryset(request.user, models.User)
|
|
team_list = get_user_queryset(request.user, models.Team)
|
|
credential_list = get_user_queryset(request.user, models.Credential)
|
|
job_template_list = get_user_queryset(request.user, models.JobTemplate)
|
|
organization_list = get_user_queryset(request.user, models.Organization)
|
|
data['users'] = {'url': reverse('api:user_list', request=request),
|
|
'total': user_list.count()}
|
|
data['organizations'] = {'url': reverse('api:organization_list', request=request),
|
|
'total': organization_list.count()}
|
|
data['teams'] = {'url': reverse('api:team_list', request=request),
|
|
'total': team_list.count()}
|
|
data['credentials'] = {'url': reverse('api:credential_list', request=request),
|
|
'total': credential_list.count()}
|
|
data['job_templates'] = {'url': reverse('api:job_template_list', request=request),
|
|
'total': job_template_list.count()}
|
|
return Response(data)
|
|
|
|
|
|
class DashboardJobsGraphView(APIView):
|
|
|
|
name = _("Dashboard Jobs Graphs")
|
|
swagger_topic = 'Jobs'
|
|
|
|
def get(self, request, format=None):
|
|
period = request.query_params.get('period', 'month')
|
|
job_type = request.query_params.get('job_type', 'all')
|
|
|
|
user_unified_jobs = get_user_queryset(request.user, models.UnifiedJob).exclude(launch_type='sync')
|
|
|
|
success_query = user_unified_jobs.filter(status='successful')
|
|
failed_query = user_unified_jobs.filter(status='failed')
|
|
|
|
if job_type == 'inv_sync':
|
|
success_query = success_query.filter(instance_of=models.InventoryUpdate)
|
|
failed_query = failed_query.filter(instance_of=models.InventoryUpdate)
|
|
elif job_type == 'playbook_run':
|
|
success_query = success_query.filter(instance_of=models.Job)
|
|
failed_query = failed_query.filter(instance_of=models.Job)
|
|
elif job_type == 'scm_update':
|
|
success_query = success_query.filter(instance_of=models.ProjectUpdate)
|
|
failed_query = failed_query.filter(instance_of=models.ProjectUpdate)
|
|
|
|
success_qss = qsstats.QuerySetStats(success_query, 'finished')
|
|
failed_qss = qsstats.QuerySetStats(failed_query, 'finished')
|
|
|
|
start_date = now()
|
|
if period == 'month':
|
|
end_date = start_date - dateutil.relativedelta.relativedelta(months=1)
|
|
interval = 'days'
|
|
elif period == 'two_weeks':
|
|
end_date = start_date - dateutil.relativedelta.relativedelta(weeks=2)
|
|
interval = 'days'
|
|
elif period == 'week':
|
|
end_date = start_date - dateutil.relativedelta.relativedelta(weeks=1)
|
|
interval = 'days'
|
|
elif period == 'day':
|
|
end_date = start_date - dateutil.relativedelta.relativedelta(days=1)
|
|
interval = 'hours'
|
|
else:
|
|
return Response({'error': _('Unknown period "%s"') % str(period)}, status=status.HTTP_400_BAD_REQUEST)
|
|
|
|
dashboard_data = {"jobs": {"successful": [], "failed": []}}
|
|
for element in success_qss.time_series(end_date, start_date, interval=interval):
|
|
dashboard_data['jobs']['successful'].append([time.mktime(element[0].timetuple()),
|
|
element[1]])
|
|
for element in failed_qss.time_series(end_date, start_date, interval=interval):
|
|
dashboard_data['jobs']['failed'].append([time.mktime(element[0].timetuple()),
|
|
element[1]])
|
|
return Response(dashboard_data)
|
|
|
|
|
|
class InstanceList(ListAPIView):
|
|
|
|
name = _("Instances")
|
|
model = models.Instance
|
|
serializer_class = serializers.InstanceSerializer
|
|
search_fields = ('hostname',)
|
|
|
|
|
|
class InstanceDetail(RetrieveUpdateAPIView):
|
|
|
|
name = _("Instance Detail")
|
|
model = models.Instance
|
|
serializer_class = serializers.InstanceSerializer
|
|
|
|
|
|
def update(self, request, *args, **kwargs):
|
|
r = super(InstanceDetail, self).update(request, *args, **kwargs)
|
|
if status.is_success(r.status_code):
|
|
obj = self.get_object()
|
|
obj.refresh_capacity()
|
|
obj.save()
|
|
r.data = serializers.InstanceSerializer(obj, context=self.get_serializer_context()).to_representation(obj)
|
|
return r
|
|
|
|
|
|
class InstanceUnifiedJobsList(SubListAPIView):
|
|
|
|
name = _("Instance Jobs")
|
|
model = models.UnifiedJob
|
|
serializer_class = serializers.UnifiedJobListSerializer
|
|
parent_model = models.Instance
|
|
|
|
def get_queryset(self):
|
|
po = self.get_parent_object()
|
|
qs = get_user_queryset(self.request.user, models.UnifiedJob)
|
|
qs = qs.filter(execution_node=po.hostname)
|
|
return qs
|
|
|
|
|
|
class InstanceInstanceGroupsList(InstanceGroupMembershipMixin, SubListCreateAttachDetachAPIView):
|
|
|
|
name = _("Instance's Instance Groups")
|
|
model = models.InstanceGroup
|
|
serializer_class = serializers.InstanceGroupSerializer
|
|
parent_model = models.Instance
|
|
relationship = 'rampart_groups'
|
|
|
|
|
|
class InstanceGroupList(ListCreateAPIView):
|
|
|
|
name = _("Instance Groups")
|
|
model = models.InstanceGroup
|
|
serializer_class = serializers.InstanceGroupSerializer
|
|
|
|
|
|
class InstanceGroupDetail(RelatedJobsPreventDeleteMixin, RetrieveUpdateDestroyAPIView):
|
|
|
|
always_allow_superuser = False
|
|
name = _("Instance Group Detail")
|
|
model = models.InstanceGroup
|
|
serializer_class = serializers.InstanceGroupSerializer
|
|
permission_classes = (InstanceGroupTowerPermission,)
|
|
|
|
def update_raw_data(self, data):
|
|
if self.get_object().is_containerized:
|
|
data.pop('policy_instance_percentage', None)
|
|
data.pop('policy_instance_minimum', None)
|
|
data.pop('policy_instance_list', None)
|
|
return super(InstanceGroupDetail, self).update_raw_data(data)
|
|
|
|
def destroy(self, request, *args, **kwargs):
|
|
instance = self.get_object()
|
|
if instance.controller is not None:
|
|
raise PermissionDenied(detail=_("Isolated Groups can not be removed from the API"))
|
|
if instance.controlled_groups.count():
|
|
raise PermissionDenied(detail=_("Instance Groups acting as a controller for an Isolated Group can not be removed from the API"))
|
|
return super(InstanceGroupDetail, self).destroy(request, *args, **kwargs)
|
|
|
|
|
|
class InstanceGroupUnifiedJobsList(SubListAPIView):
|
|
|
|
name = _("Instance Group Running Jobs")
|
|
model = models.UnifiedJob
|
|
serializer_class = serializers.UnifiedJobListSerializer
|
|
parent_model = models.InstanceGroup
|
|
relationship = "unifiedjob_set"
|
|
|
|
|
|
class InstanceGroupInstanceList(InstanceGroupMembershipMixin, SubListAttachDetachAPIView):
|
|
|
|
name = _("Instance Group's Instances")
|
|
model = models.Instance
|
|
serializer_class = serializers.InstanceSerializer
|
|
parent_model = models.InstanceGroup
|
|
relationship = "instances"
|
|
search_fields = ('hostname',)
|
|
|
|
|
|
class ScheduleList(ListCreateAPIView):
|
|
|
|
name = _("Schedules")
|
|
model = models.Schedule
|
|
serializer_class = serializers.ScheduleSerializer
|
|
|
|
|
|
class ScheduleDetail(RetrieveUpdateDestroyAPIView):
|
|
|
|
model = models.Schedule
|
|
serializer_class = serializers.ScheduleSerializer
|
|
|
|
|
|
class SchedulePreview(GenericAPIView):
|
|
|
|
model = models.Schedule
|
|
name = _('Schedule Recurrence Rule Preview')
|
|
serializer_class = serializers.SchedulePreviewSerializer
|
|
permission_classes = (IsAuthenticated,)
|
|
|
|
def post(self, request):
|
|
serializer = self.get_serializer(data=request.data)
|
|
if serializer.is_valid():
|
|
next_stamp = now()
|
|
schedule = []
|
|
gen = models.Schedule.rrulestr(serializer.validated_data['rrule']).xafter(next_stamp, count=20)
|
|
|
|
# loop across the entire generator and grab the first 10 events
|
|
for event in gen:
|
|
if len(schedule) >= 10:
|
|
break
|
|
if not dateutil.tz.datetime_exists(event):
|
|
# skip imaginary dates, like 2:30 on DST boundaries
|
|
continue
|
|
schedule.append(event)
|
|
|
|
return Response({
|
|
'local': schedule,
|
|
'utc': [s.astimezone(pytz.utc) for s in schedule]
|
|
})
|
|
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
|
|
|
|
|
class ScheduleZoneInfo(APIView):
|
|
|
|
swagger_topic = 'System Configuration'
|
|
|
|
def get(self, request):
|
|
zones = [
|
|
{'name': zone}
|
|
for zone in models.Schedule.get_zoneinfo()
|
|
]
|
|
return Response(zones)
|
|
|
|
|
|
class LaunchConfigCredentialsBase(SubListAttachDetachAPIView):
|
|
|
|
model = models.Credential
|
|
serializer_class = serializers.CredentialSerializer
|
|
relationship = 'credentials'
|
|
|
|
def is_valid_relation(self, parent, sub, created=False):
|
|
if not parent.unified_job_template:
|
|
return {"msg": _("Cannot assign credential when related template is null.")}
|
|
|
|
ask_mapping = parent.unified_job_template.get_ask_mapping()
|
|
|
|
if self.relationship not in ask_mapping:
|
|
return {"msg": _("Related template cannot accept {} on launch.").format(self.relationship)}
|
|
elif sub.passwords_needed:
|
|
return {"msg": _("Credential that requires user input on launch "
|
|
"cannot be used in saved launch configuration.")}
|
|
|
|
ask_field_name = ask_mapping[self.relationship]
|
|
|
|
if not getattr(parent.unified_job_template, ask_field_name):
|
|
return {"msg": _("Related template is not configured to accept credentials on launch.")}
|
|
elif sub.unique_hash() in [cred.unique_hash() for cred in parent.credentials.all()]:
|
|
return {"msg": _("This launch configuration already provides a {credential_type} credential.").format(
|
|
credential_type=sub.unique_hash(display=True))}
|
|
elif sub.pk in parent.unified_job_template.credentials.values_list('pk', flat=True):
|
|
return {"msg": _("Related template already uses {credential_type} credential.").format(
|
|
credential_type=sub.name)}
|
|
|
|
# None means there were no validation errors
|
|
return None
|
|
|
|
|
|
class ScheduleCredentialsList(LaunchConfigCredentialsBase):
|
|
|
|
parent_model = models.Schedule
|
|
|
|
|
|
class ScheduleUnifiedJobsList(SubListAPIView):
|
|
|
|
model = models.UnifiedJob
|
|
serializer_class = serializers.UnifiedJobListSerializer
|
|
parent_model = models.Schedule
|
|
relationship = 'unifiedjob_set'
|
|
name = _('Schedule Jobs List')
|
|
|
|
|
|
class AuthView(APIView):
|
|
''' List enabled single-sign-on endpoints '''
|
|
|
|
authentication_classes = []
|
|
permission_classes = (AllowAny,)
|
|
swagger_topic = 'System Configuration'
|
|
|
|
def get(self, request):
|
|
from rest_framework.reverse import reverse
|
|
data = OrderedDict()
|
|
err_backend, err_message = request.session.get('social_auth_error', (None, None))
|
|
auth_backends = list(load_backends(settings.AUTHENTICATION_BACKENDS, force_load=True).items())
|
|
# Return auth backends in consistent order: Google, GitHub, SAML.
|
|
auth_backends.sort(key=lambda x: 'g' if x[0] == 'google-oauth2' else x[0])
|
|
for name, backend in auth_backends:
|
|
login_url = reverse('social:begin', args=(name,))
|
|
complete_url = request.build_absolute_uri(reverse('social:complete', args=(name,)))
|
|
backend_data = {
|
|
'login_url': login_url,
|
|
'complete_url': complete_url,
|
|
}
|
|
if name == 'saml':
|
|
backend_data['metadata_url'] = reverse('sso:saml_metadata')
|
|
for idp in sorted(settings.SOCIAL_AUTH_SAML_ENABLED_IDPS.keys()):
|
|
saml_backend_data = dict(backend_data.items())
|
|
saml_backend_data['login_url'] = '%s?idp=%s' % (login_url, idp)
|
|
full_backend_name = '%s:%s' % (name, idp)
|
|
if (err_backend == full_backend_name or err_backend == name) and err_message:
|
|
saml_backend_data['error'] = err_message
|
|
data[full_backend_name] = saml_backend_data
|
|
else:
|
|
if err_backend == name and err_message:
|
|
backend_data['error'] = err_message
|
|
data[name] = backend_data
|
|
return Response(data)
|
|
|
|
|
|
class TeamList(ListCreateAPIView):
|
|
|
|
model = models.Team
|
|
serializer_class = serializers.TeamSerializer
|
|
|
|
|
|
class TeamDetail(RetrieveUpdateDestroyAPIView):
|
|
|
|
model = models.Team
|
|
serializer_class = serializers.TeamSerializer
|
|
|
|
|
|
class TeamUsersList(BaseUsersList):
|
|
|
|
model = models.User
|
|
serializer_class = serializers.UserSerializer
|
|
parent_model = models.Team
|
|
relationship = 'member_role.members'
|
|
ordering = ('username',)
|
|
|
|
|
|
class TeamRolesList(SubListAttachDetachAPIView):
|
|
|
|
model = models.Role
|
|
serializer_class = serializers.RoleSerializerWithParentAccess
|
|
metadata_class = RoleMetadata
|
|
parent_model = models.Team
|
|
relationship='member_role.children'
|
|
search_fields = ('role_field', 'content_type__model',)
|
|
|
|
def get_queryset(self):
|
|
team = get_object_or_404(models.Team, pk=self.kwargs['pk'])
|
|
if not self.request.user.can_access(models.Team, 'read', team):
|
|
raise PermissionDenied()
|
|
return models.Role.filter_visible_roles(self.request.user, team.member_role.children.all().exclude(pk=team.read_role.pk))
|
|
|
|
def post(self, request, *args, **kwargs):
|
|
sub_id = request.data.get('id', None)
|
|
if not sub_id:
|
|
return super(TeamRolesList, self).post(request)
|
|
|
|
role = get_object_or_400(models.Role, pk=sub_id)
|
|
org_content_type = ContentType.objects.get_for_model(models.Organization)
|
|
if role.content_type == org_content_type and role.role_field in ['member_role', 'admin_role']:
|
|
data = dict(msg=_("You cannot assign an Organization participation role as a child role for a Team."))
|
|
return Response(data, status=status.HTTP_400_BAD_REQUEST)
|
|
|
|
if role.is_singleton():
|
|
data = dict(msg=_("You cannot grant system-level permissions to a team."))
|
|
return Response(data, status=status.HTTP_400_BAD_REQUEST)
|
|
|
|
team = get_object_or_404(models.Team, pk=self.kwargs['pk'])
|
|
credential_content_type = ContentType.objects.get_for_model(models.Credential)
|
|
if role.content_type == credential_content_type:
|
|
if not role.content_object.organization or role.content_object.organization.id != team.organization.id:
|
|
data = dict(msg=_("You cannot grant credential access to a team when the Organization field isn't set, or belongs to a different organization"))
|
|
return Response(data, status=status.HTTP_400_BAD_REQUEST)
|
|
|
|
return super(TeamRolesList, self).post(request, *args, **kwargs)
|
|
|
|
|
|
class TeamObjectRolesList(SubListAPIView):
|
|
|
|
model = models.Role
|
|
serializer_class = serializers.RoleSerializer
|
|
parent_model = models.Team
|
|
search_fields = ('role_field', 'content_type__model',)
|
|
|
|
def get_queryset(self):
|
|
po = self.get_parent_object()
|
|
content_type = ContentType.objects.get_for_model(self.parent_model)
|
|
return models.Role.objects.filter(content_type=content_type, object_id=po.pk)
|
|
|
|
|
|
class TeamProjectsList(SubListAPIView):
|
|
|
|
model = models.Project
|
|
serializer_class = serializers.ProjectSerializer
|
|
parent_model = models.Team
|
|
|
|
def get_queryset(self):
|
|
team = self.get_parent_object()
|
|
self.check_parent_access(team)
|
|
model_ct = ContentType.objects.get_for_model(self.model)
|
|
parent_ct = ContentType.objects.get_for_model(self.parent_model)
|
|
proj_roles = models.Role.objects.filter(
|
|
Q(ancestors__content_type=parent_ct) & Q(ancestors__object_id=team.pk),
|
|
content_type=model_ct
|
|
)
|
|
return self.model.accessible_objects(self.request.user, 'read_role').filter(pk__in=[t.content_object.pk for t in proj_roles])
|
|
|
|
|
|
class TeamActivityStreamList(SubListAPIView):
|
|
|
|
model = models.ActivityStream
|
|
serializer_class = serializers.ActivityStreamSerializer
|
|
parent_model = models.Team
|
|
relationship = 'activitystream_set'
|
|
search_fields = ('changes',)
|
|
|
|
def get_queryset(self):
|
|
parent = self.get_parent_object()
|
|
self.check_parent_access(parent)
|
|
|
|
qs = self.request.user.get_queryset(self.model)
|
|
return qs.filter(Q(team=parent) |
|
|
Q(project__in=models.Project.accessible_objects(parent, 'read_role')) |
|
|
Q(credential__in=models.Credential.accessible_objects(parent, 'read_role')))
|
|
|
|
|
|
class TeamAccessList(ResourceAccessList):
|
|
|
|
model = models.User # needs to be User for AccessLists's
|
|
parent_model = models.Team
|
|
|
|
|
|
class ProjectList(ListCreateAPIView):
|
|
|
|
model = models.Project
|
|
serializer_class = serializers.ProjectSerializer
|
|
|
|
|
|
class ProjectDetail(RelatedJobsPreventDeleteMixin, RetrieveUpdateDestroyAPIView):
|
|
|
|
model = models.Project
|
|
serializer_class = serializers.ProjectSerializer
|
|
|
|
|
|
class ProjectPlaybooks(RetrieveAPIView):
|
|
|
|
model = models.Project
|
|
serializer_class = serializers.ProjectPlaybooksSerializer
|
|
|
|
|
|
class ProjectInventories(RetrieveAPIView):
|
|
|
|
model = models.Project
|
|
serializer_class = serializers.ProjectInventoriesSerializer
|
|
|
|
|
|
class ProjectTeamsList(ListAPIView):
|
|
|
|
model = models.Team
|
|
serializer_class = serializers.TeamSerializer
|
|
|
|
def get_queryset(self):
|
|
p = get_object_or_404(models.Project, pk=self.kwargs['pk'])
|
|
if not self.request.user.can_access(models.Project, 'read', p):
|
|
raise PermissionDenied()
|
|
project_ct = ContentType.objects.get_for_model(models.Project)
|
|
team_ct = ContentType.objects.get_for_model(self.model)
|
|
all_roles = models.Role.objects.filter(Q(descendents__content_type=project_ct) & Q(descendents__object_id=p.pk), content_type=team_ct)
|
|
return self.model.accessible_objects(self.request.user, 'read_role').filter(pk__in=[t.content_object.pk for t in all_roles])
|
|
|
|
|
|
class ProjectSchedulesList(SubListCreateAPIView):
|
|
|
|
name = _("Project Schedules")
|
|
|
|
model = models.Schedule
|
|
serializer_class = serializers.ScheduleSerializer
|
|
parent_model = models.Project
|
|
relationship = 'schedules'
|
|
parent_key = 'unified_job_template'
|
|
|
|
|
|
class ProjectScmInventorySources(SubListAPIView):
|
|
|
|
name = _("Project SCM Inventory Sources")
|
|
model = models.InventorySource
|
|
serializer_class = serializers.InventorySourceSerializer
|
|
parent_model = models.Project
|
|
relationship = 'scm_inventory_sources'
|
|
parent_key = 'source_project'
|
|
|
|
|
|
class ProjectActivityStreamList(SubListAPIView):
|
|
|
|
model = models.ActivityStream
|
|
serializer_class = serializers.ActivityStreamSerializer
|
|
parent_model = models.Project
|
|
relationship = 'activitystream_set'
|
|
search_fields = ('changes',)
|
|
|
|
def get_queryset(self):
|
|
parent = self.get_parent_object()
|
|
self.check_parent_access(parent)
|
|
qs = self.request.user.get_queryset(self.model)
|
|
if parent is None:
|
|
return qs
|
|
elif parent.credential is None:
|
|
return qs.filter(project=parent)
|
|
return qs.filter(Q(project=parent) | Q(credential=parent.credential))
|
|
|
|
|
|
class ProjectNotificationTemplatesAnyList(SubListCreateAttachDetachAPIView):
|
|
|
|
model = models.NotificationTemplate
|
|
serializer_class = serializers.NotificationTemplateSerializer
|
|
parent_model = models.Project
|
|
|
|
|
|
class ProjectNotificationTemplatesStartedList(ProjectNotificationTemplatesAnyList):
|
|
|
|
relationship = 'notification_templates_started'
|
|
|
|
|
|
class ProjectNotificationTemplatesErrorList(ProjectNotificationTemplatesAnyList):
|
|
|
|
relationship = 'notification_templates_error'
|
|
|
|
|
|
class ProjectNotificationTemplatesSuccessList(ProjectNotificationTemplatesAnyList):
|
|
|
|
relationship = 'notification_templates_success'
|
|
|
|
|
|
class ProjectUpdatesList(SubListAPIView):
|
|
|
|
model = models.ProjectUpdate
|
|
serializer_class = serializers.ProjectUpdateListSerializer
|
|
parent_model = models.Project
|
|
relationship = 'project_updates'
|
|
|
|
|
|
class ProjectUpdateView(RetrieveAPIView):
|
|
|
|
model = models.Project
|
|
serializer_class = serializers.ProjectUpdateViewSerializer
|
|
permission_classes = (ProjectUpdatePermission,)
|
|
|
|
def post(self, request, *args, **kwargs):
|
|
obj = self.get_object()
|
|
if obj.can_update:
|
|
project_update = obj.update()
|
|
if not project_update:
|
|
return Response({}, status=status.HTTP_400_BAD_REQUEST)
|
|
else:
|
|
data = OrderedDict()
|
|
data['project_update'] = project_update.id
|
|
data.update(
|
|
serializers.ProjectUpdateSerializer(project_update, context=self.get_serializer_context()).to_representation(project_update)
|
|
)
|
|
headers = {'Location': project_update.get_absolute_url(request=request)}
|
|
return Response(data,
|
|
headers=headers,
|
|
status=status.HTTP_202_ACCEPTED)
|
|
else:
|
|
return self.http_method_not_allowed(request, *args, **kwargs)
|
|
|
|
|
|
class ProjectUpdateList(ListAPIView):
|
|
|
|
model = models.ProjectUpdate
|
|
serializer_class = serializers.ProjectUpdateListSerializer
|
|
|
|
|
|
class ProjectUpdateDetail(UnifiedJobDeletionMixin, RetrieveDestroyAPIView):
|
|
|
|
model = models.ProjectUpdate
|
|
serializer_class = serializers.ProjectUpdateDetailSerializer
|
|
|
|
|
|
class ProjectUpdateEventsList(SubListAPIView):
|
|
|
|
model = models.ProjectUpdateEvent
|
|
serializer_class = serializers.ProjectUpdateEventSerializer
|
|
parent_model = models.ProjectUpdate
|
|
relationship = 'project_update_events'
|
|
name = _('Project Update Events List')
|
|
search_fields = ('stdout',)
|
|
|
|
def finalize_response(self, request, response, *args, **kwargs):
|
|
response['X-UI-Max-Events'] = settings.MAX_UI_JOB_EVENTS
|
|
return super(ProjectUpdateEventsList, self).finalize_response(request, response, *args, **kwargs)
|
|
|
|
|
|
class SystemJobEventsList(SubListAPIView):
|
|
|
|
model = models.SystemJobEvent
|
|
serializer_class = serializers.SystemJobEventSerializer
|
|
parent_model = models.SystemJob
|
|
relationship = 'system_job_events'
|
|
name = _('System Job Events List')
|
|
search_fields = ('stdout',)
|
|
|
|
def finalize_response(self, request, response, *args, **kwargs):
|
|
response['X-UI-Max-Events'] = settings.MAX_UI_JOB_EVENTS
|
|
return super(SystemJobEventsList, self).finalize_response(request, response, *args, **kwargs)
|
|
|
|
|
|
class ProjectUpdateCancel(RetrieveAPIView):
|
|
|
|
model = models.ProjectUpdate
|
|
obj_permission_type = 'cancel'
|
|
serializer_class = serializers.ProjectUpdateCancelSerializer
|
|
|
|
def post(self, request, *args, **kwargs):
|
|
obj = self.get_object()
|
|
if obj.can_cancel:
|
|
obj.cancel()
|
|
return Response(status=status.HTTP_202_ACCEPTED)
|
|
else:
|
|
return self.http_method_not_allowed(request, *args, **kwargs)
|
|
|
|
|
|
class ProjectUpdateNotificationsList(SubListAPIView):
|
|
|
|
model = models.Notification
|
|
serializer_class = serializers.NotificationSerializer
|
|
parent_model = models.ProjectUpdate
|
|
relationship = 'notifications'
|
|
search_fields = ('subject', 'notification_type', 'body',)
|
|
|
|
|
|
class ProjectUpdateScmInventoryUpdates(SubListAPIView):
|
|
|
|
name = _("Project Update SCM Inventory Updates")
|
|
model = models.InventoryUpdate
|
|
serializer_class = serializers.InventoryUpdateListSerializer
|
|
parent_model = models.ProjectUpdate
|
|
relationship = 'scm_inventory_updates'
|
|
parent_key = 'source_project_update'
|
|
|
|
|
|
class ProjectAccessList(ResourceAccessList):
|
|
|
|
model = models.User # needs to be User for AccessLists's
|
|
parent_model = models.Project
|
|
|
|
|
|
class ProjectObjectRolesList(SubListAPIView):
|
|
|
|
model = models.Role
|
|
serializer_class = serializers.RoleSerializer
|
|
parent_model = models.Project
|
|
search_fields = ('role_field', 'content_type__model',)
|
|
|
|
def get_queryset(self):
|
|
po = self.get_parent_object()
|
|
content_type = ContentType.objects.get_for_model(self.parent_model)
|
|
return models.Role.objects.filter(content_type=content_type, object_id=po.pk)
|
|
|
|
|
|
class ProjectCopy(CopyAPIView):
|
|
|
|
model = models.Project
|
|
copy_return_serializer_class = serializers.ProjectSerializer
|
|
|
|
|
|
class UserList(ListCreateAPIView):
|
|
|
|
model = models.User
|
|
serializer_class = serializers.UserSerializer
|
|
permission_classes = (UserPermission,)
|
|
ordering = ('username',)
|
|
|
|
|
|
class UserMeList(ListAPIView):
|
|
|
|
model = models.User
|
|
serializer_class = serializers.UserSerializer
|
|
name = _('Me')
|
|
ordering = ('username',)
|
|
|
|
def get_queryset(self):
|
|
return self.model.objects.filter(pk=self.request.user.pk)
|
|
|
|
|
|
class OAuth2ApplicationList(ListCreateAPIView):
|
|
|
|
name = _("OAuth 2 Applications")
|
|
|
|
model = models.OAuth2Application
|
|
serializer_class = serializers.OAuth2ApplicationSerializer
|
|
swagger_topic = 'Authentication'
|
|
|
|
|
|
class OAuth2ApplicationDetail(RetrieveUpdateDestroyAPIView):
|
|
|
|
name = _("OAuth 2 Application Detail")
|
|
|
|
model = models.OAuth2Application
|
|
serializer_class = serializers.OAuth2ApplicationSerializer
|
|
swagger_topic = 'Authentication'
|
|
|
|
def update_raw_data(self, data):
|
|
data.pop('client_secret', None)
|
|
return super(OAuth2ApplicationDetail, self).update_raw_data(data)
|
|
|
|
|
|
class ApplicationOAuth2TokenList(SubListCreateAPIView):
|
|
|
|
name = _("OAuth 2 Application Tokens")
|
|
|
|
model = models.OAuth2AccessToken
|
|
serializer_class = serializers.OAuth2TokenSerializer
|
|
parent_model = models.OAuth2Application
|
|
relationship = 'oauth2accesstoken_set'
|
|
parent_key = 'application'
|
|
swagger_topic = 'Authentication'
|
|
|
|
|
|
class OAuth2ApplicationActivityStreamList(SubListAPIView):
|
|
|
|
model = models.ActivityStream
|
|
serializer_class = serializers.ActivityStreamSerializer
|
|
parent_model = models.OAuth2Application
|
|
relationship = 'activitystream_set'
|
|
swagger_topic = 'Authentication'
|
|
search_fields = ('changes',)
|
|
|
|
|
|
class OAuth2TokenList(ListCreateAPIView):
|
|
|
|
name = _("OAuth2 Tokens")
|
|
|
|
model = models.OAuth2AccessToken
|
|
serializer_class = serializers.OAuth2TokenSerializer
|
|
swagger_topic = 'Authentication'
|
|
|
|
|
|
class OAuth2UserTokenList(SubListCreateAPIView):
|
|
|
|
name = _("OAuth2 User Tokens")
|
|
|
|
model = models.OAuth2AccessToken
|
|
serializer_class = serializers.OAuth2TokenSerializer
|
|
parent_model = models.User
|
|
relationship = 'main_oauth2accesstoken'
|
|
parent_key = 'user'
|
|
swagger_topic = 'Authentication'
|
|
|
|
|
|
class UserAuthorizedTokenList(SubListCreateAPIView):
|
|
|
|
name = _("OAuth2 User Authorized Access Tokens")
|
|
|
|
model = models.OAuth2AccessToken
|
|
serializer_class = serializers.UserAuthorizedTokenSerializer
|
|
parent_model = models.User
|
|
relationship = 'oauth2accesstoken_set'
|
|
parent_key = 'user'
|
|
swagger_topic = 'Authentication'
|
|
|
|
def get_queryset(self):
|
|
return get_access_token_model().objects.filter(application__isnull=False, user=self.request.user)
|
|
|
|
|
|
class OrganizationApplicationList(SubListCreateAPIView):
|
|
|
|
name = _("Organization OAuth2 Applications")
|
|
|
|
model = models.OAuth2Application
|
|
serializer_class = serializers.OAuth2ApplicationSerializer
|
|
parent_model = models.Organization
|
|
relationship = 'applications'
|
|
parent_key = 'organization'
|
|
swagger_topic = 'Authentication'
|
|
|
|
|
|
class UserPersonalTokenList(SubListCreateAPIView):
|
|
|
|
name = _("OAuth2 Personal Access Tokens")
|
|
|
|
model = models.OAuth2AccessToken
|
|
serializer_class = serializers.UserPersonalTokenSerializer
|
|
parent_model = models.User
|
|
relationship = 'main_oauth2accesstoken'
|
|
parent_key = 'user'
|
|
swagger_topic = 'Authentication'
|
|
|
|
def get_queryset(self):
|
|
return get_access_token_model().objects.filter(application__isnull=True, user=self.request.user)
|
|
|
|
|
|
class OAuth2TokenDetail(RetrieveUpdateDestroyAPIView):
|
|
|
|
name = _("OAuth Token Detail")
|
|
|
|
model = models.OAuth2AccessToken
|
|
serializer_class = serializers.OAuth2TokenDetailSerializer
|
|
swagger_topic = 'Authentication'
|
|
|
|
|
|
class OAuth2TokenActivityStreamList(SubListAPIView):
|
|
|
|
model = models.ActivityStream
|
|
serializer_class = serializers.ActivityStreamSerializer
|
|
parent_model = models.OAuth2AccessToken
|
|
relationship = 'activitystream_set'
|
|
swagger_topic = 'Authentication'
|
|
search_fields = ('changes',)
|
|
|
|
|
|
class UserTeamsList(SubListAPIView):
|
|
|
|
model = models.Team
|
|
serializer_class = serializers.TeamSerializer
|
|
parent_model = models.User
|
|
|
|
def get_queryset(self):
|
|
u = get_object_or_404(models.User, pk=self.kwargs['pk'])
|
|
if not self.request.user.can_access(models.User, 'read', u):
|
|
raise PermissionDenied()
|
|
return models.Team.accessible_objects(self.request.user, 'read_role').filter(
|
|
Q(member_role__members=u) | Q(admin_role__members=u)).distinct()
|
|
|
|
|
|
class UserRolesList(SubListAttachDetachAPIView):
|
|
|
|
model = models.Role
|
|
serializer_class = serializers.RoleSerializerWithParentAccess
|
|
metadata_class = RoleMetadata
|
|
parent_model = models.User
|
|
relationship='roles'
|
|
permission_classes = (IsAuthenticated,)
|
|
search_fields = ('role_field', 'content_type__model',)
|
|
|
|
def get_queryset(self):
|
|
u = get_object_or_404(models.User, pk=self.kwargs['pk'])
|
|
if not self.request.user.can_access(models.User, 'read', u):
|
|
raise PermissionDenied()
|
|
content_type = ContentType.objects.get_for_model(models.User)
|
|
|
|
return models.Role.filter_visible_roles(
|
|
self.request.user, u.roles.all()
|
|
).exclude(content_type=content_type, object_id=u.id)
|
|
|
|
def post(self, request, *args, **kwargs):
|
|
sub_id = request.data.get('id', None)
|
|
if not sub_id:
|
|
return super(UserRolesList, self).post(request)
|
|
|
|
user = get_object_or_400(models.User, pk=self.kwargs['pk'])
|
|
role = get_object_or_400(models.Role, pk=sub_id)
|
|
|
|
credential_content_type = ContentType.objects.get_for_model(models.Credential)
|
|
if role.content_type == credential_content_type:
|
|
if 'disassociate' not in request.data and role.content_object.organization and user not in role.content_object.organization.member_role:
|
|
data = dict(msg=_("You cannot grant credential access to a user not in the credentials' organization"))
|
|
return Response(data, status=status.HTTP_400_BAD_REQUEST)
|
|
|
|
if not role.content_object.organization and not request.user.is_superuser:
|
|
data = dict(msg=_("You cannot grant private credential access to another user"))
|
|
return Response(data, status=status.HTTP_400_BAD_REQUEST)
|
|
|
|
|
|
return super(UserRolesList, self).post(request, *args, **kwargs)
|
|
|
|
def check_parent_access(self, parent=None):
|
|
# We hide roles that shouldn't be seen in our queryset
|
|
return True
|
|
|
|
|
|
class UserProjectsList(SubListAPIView):
|
|
|
|
model = models.Project
|
|
serializer_class = serializers.ProjectSerializer
|
|
parent_model = models.User
|
|
|
|
def get_queryset(self):
|
|
parent = self.get_parent_object()
|
|
self.check_parent_access(parent)
|
|
my_qs = models.Project.accessible_objects(self.request.user, 'read_role')
|
|
user_qs = models.Project.accessible_objects(parent, 'read_role')
|
|
return my_qs & user_qs
|
|
|
|
|
|
class UserOrganizationsList(OrganizationCountsMixin, SubListAPIView):
|
|
|
|
model = models.Organization
|
|
serializer_class = serializers.OrganizationSerializer
|
|
parent_model = models.User
|
|
relationship = 'organizations'
|
|
|
|
def get_queryset(self):
|
|
parent = self.get_parent_object()
|
|
self.check_parent_access(parent)
|
|
my_qs = models.Organization.accessible_objects(self.request.user, 'read_role')
|
|
user_qs = models.Organization.objects.filter(member_role__members=parent)
|
|
return my_qs & user_qs
|
|
|
|
|
|
class UserAdminOfOrganizationsList(OrganizationCountsMixin, SubListAPIView):
|
|
|
|
model = models.Organization
|
|
serializer_class = serializers.OrganizationSerializer
|
|
parent_model = models.User
|
|
relationship = 'admin_of_organizations'
|
|
|
|
def get_queryset(self):
|
|
parent = self.get_parent_object()
|
|
self.check_parent_access(parent)
|
|
my_qs = models.Organization.accessible_objects(self.request.user, 'read_role')
|
|
user_qs = models.Organization.objects.filter(admin_role__members=parent)
|
|
return my_qs & user_qs
|
|
|
|
|
|
class UserActivityStreamList(SubListAPIView):
|
|
|
|
model = models.ActivityStream
|
|
serializer_class = serializers.ActivityStreamSerializer
|
|
parent_model = models.User
|
|
relationship = 'activitystream_set'
|
|
search_fields = ('changes',)
|
|
|
|
def get_queryset(self):
|
|
parent = self.get_parent_object()
|
|
self.check_parent_access(parent)
|
|
qs = self.request.user.get_queryset(self.model)
|
|
return qs.filter(Q(actor=parent) | Q(user__in=[parent]))
|
|
|
|
|
|
class UserDetail(RetrieveUpdateDestroyAPIView):
|
|
|
|
model = models.User
|
|
serializer_class = serializers.UserSerializer
|
|
|
|
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 '''
|
|
obj = self.get_object()
|
|
can_change = request.user.can_access(models.User, 'change', obj, request.data)
|
|
can_admin = request.user.can_access(models.User, 'admin', obj, request.data)
|
|
|
|
su_only_edit_fields = ('is_superuser', 'is_system_auditor')
|
|
admin_only_edit_fields = ('username', 'is_active')
|
|
|
|
fields_to_check = ()
|
|
if not request.user.is_superuser:
|
|
fields_to_check += su_only_edit_fields
|
|
|
|
if can_change and not can_admin:
|
|
fields_to_check += admin_only_edit_fields
|
|
|
|
bad_changes = {}
|
|
for field in fields_to_check:
|
|
left = getattr(obj, field, None)
|
|
right = request.data.get(field, None)
|
|
if left is not None and right is not None and left != right:
|
|
bad_changes[field] = (left, right)
|
|
if bad_changes:
|
|
raise PermissionDenied(_('Cannot change %s.') % ', '.join(bad_changes.keys()))
|
|
|
|
def destroy(self, request, *args, **kwargs):
|
|
obj = self.get_object()
|
|
can_delete = request.user.can_access(models.User, 'delete', obj)
|
|
if not can_delete:
|
|
raise PermissionDenied(_('Cannot delete user.'))
|
|
return super(UserDetail, self).destroy(request, *args, **kwargs)
|
|
|
|
|
|
class UserAccessList(ResourceAccessList):
|
|
|
|
model = models.User # needs to be User for AccessLists's
|
|
parent_model = models.User
|
|
|
|
|
|
class CredentialTypeList(ListCreateAPIView):
|
|
|
|
model = models.CredentialType
|
|
serializer_class = serializers.CredentialTypeSerializer
|
|
|
|
|
|
class CredentialTypeDetail(RetrieveUpdateDestroyAPIView):
|
|
|
|
model = models.CredentialType
|
|
serializer_class = serializers.CredentialTypeSerializer
|
|
|
|
def destroy(self, request, *args, **kwargs):
|
|
instance = self.get_object()
|
|
if instance.managed_by_tower:
|
|
raise PermissionDenied(detail=_("Deletion not allowed for managed credential types"))
|
|
if instance.credentials.exists():
|
|
raise PermissionDenied(detail=_("Credential types that are in use cannot be deleted"))
|
|
return super(CredentialTypeDetail, self).destroy(request, *args, **kwargs)
|
|
|
|
|
|
class CredentialTypeCredentialList(SubListCreateAPIView):
|
|
|
|
model = models.Credential
|
|
parent_model = models.CredentialType
|
|
relationship = 'credentials'
|
|
serializer_class = serializers.CredentialSerializer
|
|
|
|
|
|
class CredentialTypeActivityStreamList(SubListAPIView):
|
|
|
|
model = models.ActivityStream
|
|
serializer_class = serializers.ActivityStreamSerializer
|
|
parent_model = models.CredentialType
|
|
relationship = 'activitystream_set'
|
|
search_fields = ('changes',)
|
|
|
|
|
|
class CredentialList(ListCreateAPIView):
|
|
|
|
model = models.Credential
|
|
serializer_class = serializers.CredentialSerializerCreate
|
|
|
|
|
|
class CredentialOwnerUsersList(SubListAPIView):
|
|
|
|
model = models.User
|
|
serializer_class = serializers.UserSerializer
|
|
parent_model = models.Credential
|
|
relationship = 'admin_role.members'
|
|
ordering = ('username',)
|
|
|
|
|
|
class CredentialOwnerTeamsList(SubListAPIView):
|
|
|
|
model = models.Team
|
|
serializer_class = serializers.TeamSerializer
|
|
parent_model = models.Credential
|
|
|
|
def get_queryset(self):
|
|
credential = get_object_or_404(self.parent_model, pk=self.kwargs['pk'])
|
|
if not self.request.user.can_access(models.Credential, 'read', credential):
|
|
raise PermissionDenied()
|
|
|
|
content_type = ContentType.objects.get_for_model(self.model)
|
|
teams = [c.content_object.pk for c in credential.admin_role.parents.filter(content_type=content_type)]
|
|
|
|
return self.model.objects.filter(pk__in=teams)
|
|
|
|
|
|
class UserCredentialsList(SubListCreateAPIView):
|
|
|
|
model = models.Credential
|
|
serializer_class = serializers.UserCredentialSerializerCreate
|
|
parent_model = models.User
|
|
parent_key = 'user'
|
|
|
|
def get_queryset(self):
|
|
user = self.get_parent_object()
|
|
self.check_parent_access(user)
|
|
|
|
visible_creds = models.Credential.accessible_objects(self.request.user, 'read_role')
|
|
user_creds = models.Credential.accessible_objects(user, 'read_role')
|
|
return user_creds & visible_creds
|
|
|
|
|
|
class TeamCredentialsList(SubListCreateAPIView):
|
|
|
|
model = models.Credential
|
|
serializer_class = serializers.TeamCredentialSerializerCreate
|
|
parent_model = models.Team
|
|
parent_key = 'team'
|
|
|
|
def get_queryset(self):
|
|
team = self.get_parent_object()
|
|
self.check_parent_access(team)
|
|
|
|
visible_creds = models.Credential.accessible_objects(self.request.user, 'read_role')
|
|
team_creds = models.Credential.objects.filter(Q(use_role__parents=team.member_role) | Q(admin_role__parents=team.member_role))
|
|
return (team_creds & visible_creds).distinct()
|
|
|
|
|
|
class OrganizationCredentialList(SubListCreateAPIView):
|
|
|
|
model = models.Credential
|
|
serializer_class = serializers.OrganizationCredentialSerializerCreate
|
|
parent_model = models.Organization
|
|
parent_key = 'organization'
|
|
|
|
def get_queryset(self):
|
|
organization = self.get_parent_object()
|
|
self.check_parent_access(organization)
|
|
|
|
user_visible = models.Credential.accessible_objects(self.request.user, 'read_role').all()
|
|
org_set = models.Credential.accessible_objects(organization.admin_role, 'read_role').all()
|
|
|
|
if self.request.user.is_superuser or self.request.user.is_system_auditor:
|
|
return org_set
|
|
|
|
return org_set & user_visible
|
|
|
|
|
|
class CredentialDetail(RetrieveUpdateDestroyAPIView):
|
|
|
|
model = models.Credential
|
|
serializer_class = serializers.CredentialSerializer
|
|
|
|
def destroy(self, request, *args, **kwargs):
|
|
instance = self.get_object()
|
|
if instance.managed_by_tower:
|
|
raise PermissionDenied(detail=_("Deletion not allowed for managed credentials"))
|
|
return super(CredentialDetail, self).destroy(request, *args, **kwargs)
|
|
|
|
|
|
|
|
class CredentialActivityStreamList(SubListAPIView):
|
|
|
|
model = models.ActivityStream
|
|
serializer_class = serializers.ActivityStreamSerializer
|
|
parent_model = models.Credential
|
|
relationship = 'activitystream_set'
|
|
search_fields = ('changes',)
|
|
|
|
|
|
class CredentialAccessList(ResourceAccessList):
|
|
|
|
model = models.User # needs to be User for AccessLists's
|
|
parent_model = models.Credential
|
|
|
|
|
|
class CredentialObjectRolesList(SubListAPIView):
|
|
|
|
model = models.Role
|
|
serializer_class = serializers.RoleSerializer
|
|
parent_model = models.Credential
|
|
search_fields = ('role_field', 'content_type__model',)
|
|
|
|
def get_queryset(self):
|
|
po = self.get_parent_object()
|
|
content_type = ContentType.objects.get_for_model(self.parent_model)
|
|
return models.Role.objects.filter(content_type=content_type, object_id=po.pk)
|
|
|
|
|
|
class CredentialCopy(CopyAPIView):
|
|
|
|
model = models.Credential
|
|
copy_return_serializer_class = serializers.CredentialSerializer
|
|
|
|
|
|
class CredentialExternalTest(SubDetailAPIView):
|
|
"""
|
|
Test updates to the input values and metadata of an external credential
|
|
before saving them.
|
|
"""
|
|
|
|
name = _('External Credential Test')
|
|
|
|
model = models.Credential
|
|
serializer_class = serializers.EmptySerializer
|
|
obj_permission_type = 'use'
|
|
|
|
def post(self, request, *args, **kwargs):
|
|
obj = self.get_object()
|
|
backend_kwargs = {}
|
|
for field_name, value in obj.inputs.items():
|
|
backend_kwargs[field_name] = obj.get_input(field_name)
|
|
for field_name, value in request.data.get('inputs', {}).items():
|
|
if value != '$encrypted$':
|
|
backend_kwargs[field_name] = value
|
|
backend_kwargs.update(request.data.get('metadata', {}))
|
|
try:
|
|
obj.credential_type.plugin.backend(**backend_kwargs)
|
|
return Response({}, status=status.HTTP_202_ACCEPTED)
|
|
except requests.exceptions.HTTPError as exc:
|
|
message = 'HTTP {}'.format(exc.response.status_code)
|
|
return Response({'inputs': message}, status=status.HTTP_400_BAD_REQUEST)
|
|
except Exception as exc:
|
|
message = exc.__class__.__name__
|
|
args = getattr(exc, 'args', [])
|
|
for a in args:
|
|
if isinstance(
|
|
getattr(a, 'reason', None),
|
|
ConnectTimeoutError
|
|
):
|
|
message = str(a.reason)
|
|
return Response({'inputs': message}, status=status.HTTP_400_BAD_REQUEST)
|
|
|
|
|
|
class CredentialInputSourceDetail(RetrieveUpdateDestroyAPIView):
|
|
|
|
name = _("Credential Input Source Detail")
|
|
|
|
model = models.CredentialInputSource
|
|
serializer_class = serializers.CredentialInputSourceSerializer
|
|
|
|
|
|
class CredentialInputSourceList(ListCreateAPIView):
|
|
|
|
name = _("Credential Input Sources")
|
|
|
|
model = models.CredentialInputSource
|
|
serializer_class = serializers.CredentialInputSourceSerializer
|
|
|
|
|
|
class CredentialInputSourceSubList(SubListCreateAPIView):
|
|
|
|
name = _("Credential Input Sources")
|
|
|
|
model = models.CredentialInputSource
|
|
serializer_class = serializers.CredentialInputSourceSerializer
|
|
parent_model = models.Credential
|
|
relationship = 'input_sources'
|
|
parent_key = 'target_credential'
|
|
|
|
|
|
class CredentialTypeExternalTest(SubDetailAPIView):
|
|
"""
|
|
Test a complete set of input values for an external credential before
|
|
saving it.
|
|
"""
|
|
|
|
name = _('External Credential Type Test')
|
|
|
|
model = models.CredentialType
|
|
serializer_class = serializers.EmptySerializer
|
|
|
|
def post(self, request, *args, **kwargs):
|
|
obj = self.get_object()
|
|
backend_kwargs = request.data.get('inputs', {})
|
|
backend_kwargs.update(request.data.get('metadata', {}))
|
|
try:
|
|
obj.plugin.backend(**backend_kwargs)
|
|
return Response({}, status=status.HTTP_202_ACCEPTED)
|
|
except requests.exceptions.HTTPError as exc:
|
|
message = 'HTTP {}'.format(exc.response.status_code)
|
|
return Response({'inputs': message}, status=status.HTTP_400_BAD_REQUEST)
|
|
except Exception as exc:
|
|
message = exc.__class__.__name__
|
|
args = getattr(exc, 'args', [])
|
|
for a in args:
|
|
if isinstance(
|
|
getattr(a, 'reason', None),
|
|
ConnectTimeoutError
|
|
):
|
|
message = str(a.reason)
|
|
return Response({'inputs': message}, status=status.HTTP_400_BAD_REQUEST)
|
|
|
|
|
|
class HostRelatedSearchMixin(object):
|
|
|
|
@property
|
|
def related_search_fields(self):
|
|
# Edge-case handle: https://github.com/ansible/ansible-tower/issues/7712
|
|
ret = super(HostRelatedSearchMixin, self).related_search_fields
|
|
ret.append('ansible_facts')
|
|
return ret
|
|
|
|
|
|
class HostList(HostRelatedSearchMixin, ListCreateAPIView):
|
|
|
|
always_allow_superuser = False
|
|
model = models.Host
|
|
serializer_class = serializers.HostSerializer
|
|
|
|
def get_queryset(self):
|
|
qs = super(HostList, self).get_queryset()
|
|
filter_string = self.request.query_params.get('host_filter', None)
|
|
if filter_string:
|
|
filter_qs = SmartFilter.query_from_string(filter_string)
|
|
qs &= filter_qs
|
|
return qs.distinct()
|
|
|
|
def list(self, *args, **kwargs):
|
|
try:
|
|
return super(HostList, self).list(*args, **kwargs)
|
|
except Exception as e:
|
|
return Response(dict(error=_(str(e))), status=status.HTTP_400_BAD_REQUEST)
|
|
|
|
|
|
class HostDetail(RelatedJobsPreventDeleteMixin, ControlledByScmMixin, RetrieveUpdateDestroyAPIView):
|
|
|
|
always_allow_superuser = False
|
|
model = models.Host
|
|
serializer_class = serializers.HostSerializer
|
|
|
|
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)
|
|
return super(HostDetail, self).delete(request, *args, **kwargs)
|
|
|
|
|
|
class HostAnsibleFactsDetail(RetrieveAPIView):
|
|
|
|
model = models.Host
|
|
serializer_class = serializers.AnsibleFactsSerializer
|
|
|
|
|
|
class InventoryHostsList(HostRelatedSearchMixin, SubListCreateAttachDetachAPIView):
|
|
|
|
model = models.Host
|
|
serializer_class = serializers.HostSerializer
|
|
parent_model = models.Inventory
|
|
relationship = 'hosts'
|
|
parent_key = 'inventory'
|
|
|
|
def get_queryset(self):
|
|
inventory = self.get_parent_object()
|
|
qs = getattrd(inventory, self.relationship).all()
|
|
# Apply queryset optimizations
|
|
qs = qs.select_related(*HostAccess.select_related).prefetch_related(*HostAccess.prefetch_related)
|
|
return qs
|
|
|
|
|
|
class HostGroupsList(ControlledByScmMixin, SubListCreateAttachDetachAPIView):
|
|
''' the list of groups a host is directly a member of '''
|
|
|
|
model = models.Group
|
|
serializer_class = serializers.GroupSerializer
|
|
parent_model = models.Host
|
|
relationship = 'groups'
|
|
|
|
def update_raw_data(self, data):
|
|
data.pop('inventory', None)
|
|
return super(HostGroupsList, self).update_raw_data(data)
|
|
|
|
def create(self, request, *args, **kwargs):
|
|
# Inject parent host inventory ID into new group data.
|
|
data = request.data
|
|
# HACK: Make request data mutable.
|
|
if getattr(data, '_mutable', None) is False:
|
|
data._mutable = True
|
|
data['inventory'] = self.get_parent_object().inventory_id
|
|
return super(HostGroupsList, self).create(request, *args, **kwargs)
|
|
|
|
|
|
class HostAllGroupsList(SubListAPIView):
|
|
''' the list of all groups of which the host is directly or indirectly a member '''
|
|
|
|
model = models.Group
|
|
serializer_class = serializers.GroupSerializer
|
|
parent_model = models.Host
|
|
relationship = 'groups'
|
|
|
|
def get_queryset(self):
|
|
parent = self.get_parent_object()
|
|
self.check_parent_access(parent)
|
|
qs = self.request.user.get_queryset(self.model).distinct()
|
|
sublist_qs = parent.all_groups.distinct()
|
|
return qs & sublist_qs
|
|
|
|
|
|
class HostInventorySourcesList(SubListAPIView):
|
|
|
|
model = models.InventorySource
|
|
serializer_class = serializers.InventorySourceSerializer
|
|
parent_model = models.Host
|
|
relationship = 'inventory_sources'
|
|
|
|
|
|
class HostSmartInventoriesList(SubListAPIView):
|
|
model = models.Inventory
|
|
serializer_class = serializers.InventorySerializer
|
|
parent_model = models.Host
|
|
relationship = 'smart_inventories'
|
|
|
|
|
|
class HostActivityStreamList(SubListAPIView):
|
|
|
|
model = models.ActivityStream
|
|
serializer_class = serializers.ActivityStreamSerializer
|
|
parent_model = models.Host
|
|
relationship = 'activitystream_set'
|
|
search_fields = ('changes',)
|
|
|
|
def get_queryset(self):
|
|
parent = self.get_parent_object()
|
|
self.check_parent_access(parent)
|
|
qs = self.request.user.get_queryset(self.model)
|
|
return qs.filter(Q(host=parent) | Q(inventory=parent.inventory))
|
|
|
|
|
|
class BadGateway(APIException):
|
|
status_code = status.HTTP_502_BAD_GATEWAY
|
|
default_detail = ''
|
|
default_code = 'bad_gateway'
|
|
|
|
|
|
class GatewayTimeout(APIException):
|
|
status_code = status.HTTP_504_GATEWAY_TIMEOUT
|
|
default_detail = ''
|
|
default_code = 'gateway_timeout'
|
|
|
|
|
|
class HostInsights(GenericAPIView):
|
|
|
|
model = models.Host
|
|
serializer_class = serializers.EmptySerializer
|
|
|
|
def _call_insights_api(self, url, session, headers):
|
|
try:
|
|
with set_environ(**settings.AWX_TASK_ENV):
|
|
res = session.get(url, headers=headers, timeout=120)
|
|
except requests.exceptions.SSLError:
|
|
raise BadGateway(_('SSLError while trying to connect to {}').format(url))
|
|
except requests.exceptions.Timeout:
|
|
raise GatewayTimeout(_('Request to {} timed out.').format(url))
|
|
except requests.exceptions.RequestException as e:
|
|
raise BadGateway(_('Unknown exception {} while trying to GET {}').format(e, url))
|
|
|
|
if res.status_code == 401:
|
|
raise BadGateway(
|
|
_('Unauthorized access. Please check your Insights Credential username and password.'))
|
|
elif res.status_code != 200:
|
|
raise BadGateway(
|
|
_(
|
|
'Failed to access the Insights API at URL {}.'
|
|
' Server responded with {} status code and message {}'
|
|
).format(url, res.status_code, res.content)
|
|
)
|
|
|
|
try:
|
|
return res.json()
|
|
except ValueError:
|
|
raise BadGateway(
|
|
_('Expected JSON response from Insights at URL {}'
|
|
' but instead got {}').format(url, res.content))
|
|
|
|
def _get_session(self, username, password):
|
|
session = requests.Session()
|
|
session.auth = requests.auth.HTTPBasicAuth(username, password)
|
|
|
|
return session
|
|
|
|
|
|
def _get_platform_info(self, host, session, headers):
|
|
url = '{}/api/inventory/v1/hosts?insights_id={}'.format(
|
|
settings.INSIGHTS_URL_BASE, host.insights_system_id)
|
|
res = self._call_insights_api(url, session, headers)
|
|
try:
|
|
res['results'][0]['id']
|
|
except (IndexError, KeyError):
|
|
raise NotFound(
|
|
_('Could not translate Insights system ID {}'
|
|
' into an Insights platform ID.').format(host.insights_system_id))
|
|
|
|
return res['results'][0]
|
|
|
|
def _get_reports(self, platform_id, session, headers):
|
|
url = '{}/api/insights/v1/system/{}/reports/'.format(
|
|
settings.INSIGHTS_URL_BASE, platform_id)
|
|
|
|
return self._call_insights_api(url, session, headers)
|
|
|
|
def _get_remediations(self, platform_id, session, headers):
|
|
url = '{}/api/remediations/v1/remediations?system={}'.format(
|
|
settings.INSIGHTS_URL_BASE, platform_id)
|
|
|
|
remediations = []
|
|
|
|
# Iterate over all of the pages of content.
|
|
while url:
|
|
data = self._call_insights_api(url, session, headers)
|
|
remediations.extend(data['data'])
|
|
|
|
url = data['links']['next'] # Will be `None` if this is the last page.
|
|
|
|
return remediations
|
|
|
|
def _get_insights(self, host, session, headers):
|
|
platform_info = self._get_platform_info(host, session, headers)
|
|
platform_id = platform_info['id']
|
|
reports = self._get_reports(platform_id, session, headers)
|
|
remediations = self._get_remediations(platform_id, session, headers)
|
|
|
|
return {
|
|
'insights_content': filter_insights_api_response(platform_info, reports, remediations)
|
|
}
|
|
|
|
def get(self, request, *args, **kwargs):
|
|
host = self.get_object()
|
|
cred = None
|
|
|
|
if host.insights_system_id is None:
|
|
return Response(
|
|
dict(error=_('This host is not recognized as an Insights host.')),
|
|
status=status.HTTP_404_NOT_FOUND
|
|
)
|
|
|
|
if host.inventory and host.inventory.insights_credential:
|
|
cred = host.inventory.insights_credential
|
|
else:
|
|
return Response(
|
|
dict(error=_('The Insights Credential for "{}" was not found.').format(host.inventory.name)),
|
|
status=status.HTTP_404_NOT_FOUND
|
|
)
|
|
|
|
username = cred.get_input('username', default='')
|
|
password = cred.get_input('password', default='')
|
|
session = self._get_session(username, password)
|
|
headers = get_awx_http_client_headers()
|
|
|
|
data = self._get_insights(host, session, headers)
|
|
return Response(data, status=status.HTTP_200_OK)
|
|
|
|
def handle_exception(self, exc):
|
|
# Continue supporting the slightly different way we have handled error responses on this view.
|
|
response = super().handle_exception(exc)
|
|
response.data['error'] = response.data.pop('detail')
|
|
return response
|
|
|
|
|
|
class GroupList(ListCreateAPIView):
|
|
|
|
model = models.Group
|
|
serializer_class = serializers.GroupSerializer
|
|
|
|
|
|
class EnforceParentRelationshipMixin(object):
|
|
'''
|
|
Useful when you have a self-refering ManyToManyRelationship.
|
|
* Tower uses a shallow (2-deep only) url pattern. For example:
|
|
|
|
When an object hangs off of a parent object you would have the url of the
|
|
form /api/v2/parent_model/34/child_model. If you then wanted a child of the
|
|
child model you would NOT do /api/v2/parent_model/34/child_model/87/child_child_model
|
|
Instead, you would access the child_child_model via /api/v2/child_child_model/87/
|
|
and you would create child_child_model's off of /api/v2/child_model/87/child_child_model_set
|
|
Now, when creating child_child_model related to child_model you still want to
|
|
link child_child_model to parent_model. That's what this class is for
|
|
'''
|
|
enforce_parent_relationship = ''
|
|
|
|
def update_raw_data(self, data):
|
|
data.pop(self.enforce_parent_relationship, None)
|
|
return super(EnforceParentRelationshipMixin, self).update_raw_data(data)
|
|
|
|
def create(self, request, *args, **kwargs):
|
|
# Inject parent group inventory ID into new group data.
|
|
data = request.data
|
|
# HACK: Make request data mutable.
|
|
if getattr(data, '_mutable', None) is False:
|
|
data._mutable = True
|
|
data[self.enforce_parent_relationship] = getattr(self.get_parent_object(), '%s_id' % self.enforce_parent_relationship)
|
|
return super(EnforceParentRelationshipMixin, self).create(request, *args, **kwargs)
|
|
|
|
|
|
class GroupChildrenList(ControlledByScmMixin, EnforceParentRelationshipMixin, SubListCreateAttachDetachAPIView):
|
|
|
|
model = models.Group
|
|
serializer_class = serializers.GroupSerializer
|
|
parent_model = models.Group
|
|
relationship = 'children'
|
|
enforce_parent_relationship = 'inventory'
|
|
|
|
def unattach(self, request, *args, **kwargs):
|
|
sub_id = request.data.get('id', None)
|
|
if sub_id is not None:
|
|
return super(GroupChildrenList, self).unattach(request, *args, **kwargs)
|
|
parent = self.get_parent_object()
|
|
if not request.user.can_access(self.model, 'delete', parent):
|
|
raise PermissionDenied()
|
|
parent.delete()
|
|
return Response(status=status.HTTP_204_NO_CONTENT)
|
|
|
|
def is_valid_relation(self, parent, sub, created=False):
|
|
# Prevent any cyclical group associations.
|
|
parent_pks = set(parent.all_parents.values_list('pk', flat=True))
|
|
parent_pks.add(parent.pk)
|
|
child_pks = set(sub.all_children.values_list('pk', flat=True))
|
|
child_pks.add(sub.pk)
|
|
if parent_pks & child_pks:
|
|
return {'error': _('Cyclical Group association.')}
|
|
return None
|
|
|
|
|
|
class GroupPotentialChildrenList(SubListAPIView):
|
|
|
|
model = models.Group
|
|
serializer_class = serializers.GroupSerializer
|
|
parent_model = models.Group
|
|
|
|
def get_queryset(self):
|
|
parent = self.get_parent_object()
|
|
self.check_parent_access(parent)
|
|
qs = self.request.user.get_queryset(self.model)
|
|
qs = qs.filter(inventory__pk=parent.inventory.pk)
|
|
except_pks = set([parent.pk])
|
|
except_pks.update(parent.all_parents.values_list('pk', flat=True))
|
|
except_pks.update(parent.all_children.values_list('pk', flat=True))
|
|
return qs.exclude(pk__in=except_pks)
|
|
|
|
|
|
class GroupHostsList(HostRelatedSearchMixin,
|
|
ControlledByScmMixin,
|
|
SubListCreateAttachDetachAPIView):
|
|
''' the list of hosts directly below a group '''
|
|
|
|
model = models.Host
|
|
serializer_class = serializers.HostSerializer
|
|
parent_model = models.Group
|
|
relationship = 'hosts'
|
|
|
|
def update_raw_data(self, data):
|
|
data.pop('inventory', None)
|
|
return super(GroupHostsList, self).update_raw_data(data)
|
|
|
|
def create(self, request, *args, **kwargs):
|
|
parent_group = models.Group.objects.get(id=self.kwargs['pk'])
|
|
# Inject parent group inventory ID into new host data.
|
|
request.data['inventory'] = parent_group.inventory_id
|
|
existing_hosts = models.Host.objects.filter(inventory=parent_group.inventory, name=request.data.get('name', ''))
|
|
if existing_hosts.count() > 0 and ('variables' not in request.data or
|
|
request.data['variables'] == '' or
|
|
request.data['variables'] == '{}' or
|
|
request.data['variables'] == '---'):
|
|
request.data['id'] = existing_hosts[0].id
|
|
return self.attach(request, *args, **kwargs)
|
|
return super(GroupHostsList, self).create(request, *args, **kwargs)
|
|
|
|
|
|
class GroupAllHostsList(HostRelatedSearchMixin, SubListAPIView):
|
|
''' the list of all hosts below a group, even including subgroups '''
|
|
|
|
model = models.Host
|
|
serializer_class = serializers.HostSerializer
|
|
parent_model = models.Group
|
|
relationship = 'hosts'
|
|
|
|
def get_queryset(self):
|
|
parent = self.get_parent_object()
|
|
self.check_parent_access(parent)
|
|
qs = self.request.user.get_queryset(self.model).distinct() # need distinct for '&' operator
|
|
sublist_qs = parent.all_hosts.distinct()
|
|
return qs & sublist_qs
|
|
|
|
|
|
class GroupInventorySourcesList(SubListAPIView):
|
|
|
|
model = models.InventorySource
|
|
serializer_class = serializers.InventorySourceSerializer
|
|
parent_model = models.Group
|
|
relationship = 'inventory_sources'
|
|
|
|
|
|
class GroupActivityStreamList(SubListAPIView):
|
|
|
|
model = models.ActivityStream
|
|
serializer_class = serializers.ActivityStreamSerializer
|
|
parent_model = models.Group
|
|
relationship = 'activitystream_set'
|
|
search_fields = ('changes',)
|
|
|
|
def get_queryset(self):
|
|
parent = self.get_parent_object()
|
|
self.check_parent_access(parent)
|
|
qs = self.request.user.get_queryset(self.model)
|
|
return qs.filter(Q(group=parent) | Q(host__in=parent.hosts.all()))
|
|
|
|
|
|
class GroupDetail(RelatedJobsPreventDeleteMixin, ControlledByScmMixin, RetrieveUpdateDestroyAPIView):
|
|
|
|
model = models.Group
|
|
serializer_class = serializers.GroupSerializer
|
|
|
|
def destroy(self, request, *args, **kwargs):
|
|
obj = self.get_object()
|
|
if not request.user.can_access(self.model, 'delete', obj):
|
|
raise PermissionDenied()
|
|
obj.delete_recursive()
|
|
return Response(status=status.HTTP_204_NO_CONTENT)
|
|
|
|
|
|
class InventoryGroupsList(SubListCreateAttachDetachAPIView):
|
|
|
|
model = models.Group
|
|
serializer_class = serializers.GroupSerializer
|
|
parent_model = models.Inventory
|
|
relationship = 'groups'
|
|
parent_key = 'inventory'
|
|
|
|
|
|
class InventoryRootGroupsList(SubListCreateAttachDetachAPIView):
|
|
|
|
model = models.Group
|
|
serializer_class = serializers.GroupSerializer
|
|
parent_model = models.Inventory
|
|
relationship = 'groups'
|
|
parent_key = 'inventory'
|
|
|
|
def get_queryset(self):
|
|
parent = self.get_parent_object()
|
|
self.check_parent_access(parent)
|
|
qs = self.request.user.get_queryset(self.model).distinct() # need distinct for '&' operator
|
|
return qs & parent.root_groups
|
|
|
|
|
|
class BaseVariableData(RetrieveUpdateAPIView):
|
|
|
|
parser_classes = api_settings.DEFAULT_PARSER_CLASSES + [YAMLParser]
|
|
renderer_classes = api_settings.DEFAULT_RENDERER_CLASSES + [YAMLRenderer]
|
|
permission_classes = (VariableDataPermission,)
|
|
|
|
|
|
class InventoryVariableData(BaseVariableData):
|
|
|
|
model = models.Inventory
|
|
serializer_class = serializers.InventoryVariableDataSerializer
|
|
|
|
|
|
class HostVariableData(BaseVariableData):
|
|
|
|
model = models.Host
|
|
serializer_class = serializers.HostVariableDataSerializer
|
|
|
|
|
|
class GroupVariableData(BaseVariableData):
|
|
|
|
model = models.Group
|
|
serializer_class = serializers.GroupVariableDataSerializer
|
|
|
|
|
|
class InventoryScriptView(RetrieveAPIView):
|
|
|
|
model = models.Inventory
|
|
serializer_class = serializers.InventoryScriptSerializer
|
|
permission_classes = (TaskPermission,)
|
|
filter_backends = ()
|
|
|
|
def retrieve(self, request, *args, **kwargs):
|
|
obj = self.get_object()
|
|
hostname = request.query_params.get('host', '')
|
|
hostvars = bool(request.query_params.get('hostvars', ''))
|
|
towervars = bool(request.query_params.get('towervars', ''))
|
|
show_all = bool(request.query_params.get('all', ''))
|
|
subset = request.query_params.get('subset', '')
|
|
if subset:
|
|
if not isinstance(subset, str):
|
|
raise ParseError(_('Inventory subset argument must be a string.'))
|
|
if subset.startswith('slice'):
|
|
slice_number, slice_count = models.Inventory.parse_slice_params(subset)
|
|
else:
|
|
raise ParseError(_('Subset does not use any supported syntax.'))
|
|
else:
|
|
slice_number, slice_count = 1, 1
|
|
if hostname:
|
|
hosts_q = dict(name=hostname)
|
|
if not show_all:
|
|
hosts_q['enabled'] = True
|
|
host = get_object_or_404(obj.hosts, **hosts_q)
|
|
return Response(host.variables_dict)
|
|
return Response(obj.get_script_data(
|
|
hostvars=hostvars,
|
|
towervars=towervars,
|
|
show_all=show_all,
|
|
slice_number=slice_number, slice_count=slice_count
|
|
))
|
|
|
|
|
|
class InventoryTreeView(RetrieveAPIView):
|
|
|
|
model = models.Inventory
|
|
serializer_class = serializers.GroupTreeSerializer
|
|
filter_backends = ()
|
|
|
|
def _populate_group_children(self, group_data, all_group_data_map, group_children_map):
|
|
if 'children' in group_data:
|
|
return
|
|
group_data['children'] = []
|
|
for child_id in group_children_map.get(group_data['id'], set()):
|
|
group_data['children'].append(all_group_data_map[child_id])
|
|
group_data['children'].sort(key=lambda x: x['name'])
|
|
for child_data in group_data['children']:
|
|
self._populate_group_children(child_data, all_group_data_map, group_children_map)
|
|
|
|
def retrieve(self, request, *args, **kwargs):
|
|
inventory = self.get_object()
|
|
group_children_map = inventory.get_group_children_map()
|
|
root_group_pks = inventory.root_groups.order_by('name').values_list('pk', flat=True)
|
|
groups_qs = inventory.groups
|
|
groups_qs = groups_qs.prefetch_related('inventory_sources')
|
|
all_group_data = serializers.GroupSerializer(groups_qs, many=True).data
|
|
all_group_data_map = dict((x['id'], x) for x in all_group_data)
|
|
tree_data = [all_group_data_map[x] for x in root_group_pks]
|
|
for group_data in tree_data:
|
|
self._populate_group_children(group_data, all_group_data_map,
|
|
group_children_map)
|
|
return Response(tree_data)
|
|
|
|
|
|
class InventoryInventorySourcesList(SubListCreateAPIView):
|
|
|
|
name = _('Inventory Source List')
|
|
|
|
model = models.InventorySource
|
|
serializer_class = serializers.InventorySourceSerializer
|
|
parent_model = models.Inventory
|
|
# Sometimes creation blocked by SCM inventory source restrictions
|
|
always_allow_superuser = False
|
|
relationship = 'inventory_sources'
|
|
parent_key = 'inventory'
|
|
|
|
|
|
class InventoryInventorySourcesUpdate(RetrieveAPIView):
|
|
name = _('Inventory Sources Update')
|
|
|
|
model = models.Inventory
|
|
obj_permission_type = 'start'
|
|
serializer_class = serializers.InventorySourceUpdateSerializer
|
|
permission_classes = (InventoryInventorySourcesUpdatePermission,)
|
|
|
|
def retrieve(self, request, *args, **kwargs):
|
|
inventory = self.get_object()
|
|
update_data = []
|
|
for inventory_source in inventory.inventory_sources.exclude(source=''):
|
|
details = {'inventory_source': inventory_source.pk,
|
|
'can_update': inventory_source.can_update}
|
|
update_data.append(details)
|
|
return Response(update_data)
|
|
|
|
def post(self, request, *args, **kwargs):
|
|
inventory = self.get_object()
|
|
update_data = []
|
|
successes = 0
|
|
failures = 0
|
|
for inventory_source in inventory.inventory_sources.exclude(source=''):
|
|
details = OrderedDict()
|
|
details['inventory_source'] = inventory_source.pk
|
|
details['status'] = None
|
|
if inventory_source.can_update:
|
|
update = inventory_source.update()
|
|
details.update(serializers.InventoryUpdateDetailSerializer(update, context=self.get_serializer_context()).to_representation(update))
|
|
details['status'] = 'started'
|
|
details['inventory_update'] = update.id
|
|
successes += 1
|
|
else:
|
|
if not details.get('status'):
|
|
details['status'] = _('Could not start because `can_update` returned False')
|
|
failures += 1
|
|
update_data.append(details)
|
|
if failures and successes:
|
|
status_code = status.HTTP_202_ACCEPTED
|
|
elif failures and not successes:
|
|
status_code = status.HTTP_400_BAD_REQUEST
|
|
elif not failures and not successes:
|
|
return Response({'detail': _('No inventory sources to update.')},
|
|
status=status.HTTP_400_BAD_REQUEST)
|
|
else:
|
|
status_code = status.HTTP_200_OK
|
|
return Response(update_data, status=status_code)
|
|
|
|
|
|
class InventorySourceList(ListCreateAPIView):
|
|
|
|
model = models.InventorySource
|
|
serializer_class = serializers.InventorySourceSerializer
|
|
always_allow_superuser = False
|
|
|
|
|
|
class InventorySourceDetail(RelatedJobsPreventDeleteMixin, RetrieveUpdateDestroyAPIView):
|
|
|
|
model = models.InventorySource
|
|
serializer_class = serializers.InventorySourceSerializer
|
|
|
|
|
|
class InventorySourceSchedulesList(SubListCreateAPIView):
|
|
|
|
name = _("Inventory Source Schedules")
|
|
|
|
model = models.Schedule
|
|
serializer_class = serializers.ScheduleSerializer
|
|
parent_model = models.InventorySource
|
|
relationship = 'schedules'
|
|
parent_key = 'unified_job_template'
|
|
|
|
|
|
class InventorySourceActivityStreamList(SubListAPIView):
|
|
|
|
model = models.ActivityStream
|
|
serializer_class = serializers.ActivityStreamSerializer
|
|
parent_model = models.InventorySource
|
|
relationship = 'activitystream_set'
|
|
search_fields = ('changes',)
|
|
|
|
|
|
class InventorySourceNotificationTemplatesAnyList(SubListCreateAttachDetachAPIView):
|
|
|
|
model = models.NotificationTemplate
|
|
serializer_class = serializers.NotificationTemplateSerializer
|
|
parent_model = models.InventorySource
|
|
|
|
def post(self, request, *args, **kwargs):
|
|
parent = self.get_parent_object()
|
|
if parent.source not in models.CLOUD_INVENTORY_SOURCES:
|
|
return Response(dict(msg=_("Notification Templates can only be assigned when source is one of {}.")
|
|
.format(models.CLOUD_INVENTORY_SOURCES, parent.source)),
|
|
status=status.HTTP_400_BAD_REQUEST)
|
|
return super(InventorySourceNotificationTemplatesAnyList, self).post(request, *args, **kwargs)
|
|
|
|
|
|
class InventorySourceNotificationTemplatesStartedList(InventorySourceNotificationTemplatesAnyList):
|
|
|
|
relationship = 'notification_templates_started'
|
|
|
|
|
|
class InventorySourceNotificationTemplatesErrorList(InventorySourceNotificationTemplatesAnyList):
|
|
|
|
relationship = 'notification_templates_error'
|
|
|
|
|
|
class InventorySourceNotificationTemplatesSuccessList(InventorySourceNotificationTemplatesAnyList):
|
|
|
|
relationship = 'notification_templates_success'
|
|
|
|
|
|
class InventorySourceHostsList(HostRelatedSearchMixin, SubListDestroyAPIView):
|
|
|
|
model = models.Host
|
|
serializer_class = serializers.HostSerializer
|
|
parent_model = models.InventorySource
|
|
relationship = 'hosts'
|
|
check_sub_obj_permission = False
|
|
|
|
def perform_list_destroy(self, instance_list):
|
|
inv_source = self.get_parent_object()
|
|
with ignore_inventory_computed_fields():
|
|
if not settings.ACTIVITY_STREAM_ENABLED_FOR_INVENTORY_SYNC:
|
|
from awx.main.signals import disable_activity_stream
|
|
with disable_activity_stream():
|
|
# job host summary deletion necessary to avoid deadlock
|
|
models.JobHostSummary.objects.filter(host__inventory_sources=inv_source).update(host=None)
|
|
models.Host.objects.filter(inventory_sources=inv_source).delete()
|
|
r = super(InventorySourceHostsList, self).perform_list_destroy([])
|
|
else:
|
|
# Advance delete of group-host memberships to prevent deadlock
|
|
# Activity stream doesn't record disassociation here anyway
|
|
# no signals-related reason to not bulk-delete
|
|
models.Host.groups.through.objects.filter(
|
|
host__inventory_sources=inv_source
|
|
).delete()
|
|
r = super(InventorySourceHostsList, self).perform_list_destroy(instance_list)
|
|
update_inventory_computed_fields.delay(inv_source.inventory_id)
|
|
return r
|
|
|
|
|
|
class InventorySourceGroupsList(SubListDestroyAPIView):
|
|
|
|
model = models.Group
|
|
serializer_class = serializers.GroupSerializer
|
|
parent_model = models.InventorySource
|
|
relationship = 'groups'
|
|
check_sub_obj_permission = False
|
|
|
|
def perform_list_destroy(self, instance_list):
|
|
inv_source = self.get_parent_object()
|
|
with ignore_inventory_computed_fields():
|
|
if not settings.ACTIVITY_STREAM_ENABLED_FOR_INVENTORY_SYNC:
|
|
from awx.main.signals import disable_activity_stream
|
|
with disable_activity_stream():
|
|
models.Group.objects.filter(inventory_sources=inv_source).delete()
|
|
r = super(InventorySourceGroupsList, self).perform_list_destroy([])
|
|
else:
|
|
# Advance delete of group-host memberships to prevent deadlock
|
|
# Same arguments for bulk delete as with host list
|
|
models.Group.hosts.through.objects.filter(
|
|
group__inventory_sources=inv_source
|
|
).delete()
|
|
r = super(InventorySourceGroupsList, self).perform_list_destroy(instance_list)
|
|
update_inventory_computed_fields.delay(inv_source.inventory_id)
|
|
return r
|
|
|
|
|
|
class InventorySourceUpdatesList(SubListAPIView):
|
|
|
|
model = models.InventoryUpdate
|
|
serializer_class = serializers.InventoryUpdateListSerializer
|
|
parent_model = models.InventorySource
|
|
relationship = 'inventory_updates'
|
|
|
|
|
|
class InventorySourceCredentialsList(SubListAttachDetachAPIView):
|
|
|
|
parent_model = models.InventorySource
|
|
model = models.Credential
|
|
serializer_class = serializers.CredentialSerializer
|
|
relationship = 'credentials'
|
|
|
|
def is_valid_relation(self, parent, sub, created=False):
|
|
# Inventory source credentials are exclusive with all other credentials
|
|
# subject to change for https://github.com/ansible/awx/issues/277
|
|
# or https://github.com/ansible/awx/issues/223
|
|
if parent.credentials.exists():
|
|
return {'msg': _("Source already has credential assigned.")}
|
|
error = models.InventorySource.cloud_credential_validation(parent.source, sub)
|
|
if error:
|
|
return {'msg': error}
|
|
return None
|
|
|
|
|
|
class InventorySourceUpdateView(RetrieveAPIView):
|
|
|
|
model = models.InventorySource
|
|
obj_permission_type = 'start'
|
|
serializer_class = serializers.InventorySourceUpdateSerializer
|
|
|
|
def post(self, request, *args, **kwargs):
|
|
obj = self.get_object()
|
|
if obj.can_update:
|
|
update = obj.update()
|
|
if not update:
|
|
return Response({}, status=status.HTTP_400_BAD_REQUEST)
|
|
else:
|
|
headers = {'Location': update.get_absolute_url(request=request)}
|
|
data = OrderedDict()
|
|
data['inventory_update'] = update.id
|
|
data.update(serializers.InventoryUpdateDetailSerializer(update, context=self.get_serializer_context()).to_representation(update))
|
|
return Response(data, status=status.HTTP_202_ACCEPTED, headers=headers)
|
|
else:
|
|
return self.http_method_not_allowed(request, *args, **kwargs)
|
|
|
|
|
|
class InventoryUpdateList(ListAPIView):
|
|
|
|
model = models.InventoryUpdate
|
|
serializer_class = serializers.InventoryUpdateListSerializer
|
|
|
|
|
|
class InventoryUpdateDetail(UnifiedJobDeletionMixin, RetrieveDestroyAPIView):
|
|
|
|
model = models.InventoryUpdate
|
|
serializer_class = serializers.InventoryUpdateDetailSerializer
|
|
|
|
|
|
class InventoryUpdateCredentialsList(SubListAPIView):
|
|
|
|
parent_model = models.InventoryUpdate
|
|
model = models.Credential
|
|
serializer_class = serializers.CredentialSerializer
|
|
relationship = 'credentials'
|
|
|
|
|
|
class InventoryUpdateCancel(RetrieveAPIView):
|
|
|
|
model = models.InventoryUpdate
|
|
obj_permission_type = 'cancel'
|
|
serializer_class = serializers.InventoryUpdateCancelSerializer
|
|
|
|
def post(self, request, *args, **kwargs):
|
|
obj = self.get_object()
|
|
if obj.can_cancel:
|
|
obj.cancel()
|
|
return Response(status=status.HTTP_202_ACCEPTED)
|
|
else:
|
|
return self.http_method_not_allowed(request, *args, **kwargs)
|
|
|
|
|
|
class InventoryUpdateNotificationsList(SubListAPIView):
|
|
|
|
model = models.Notification
|
|
serializer_class = serializers.NotificationSerializer
|
|
parent_model = models.InventoryUpdate
|
|
relationship = 'notifications'
|
|
search_fields = ('subject', 'notification_type', 'body',)
|
|
|
|
|
|
class JobTemplateList(ListCreateAPIView):
|
|
|
|
model = models.JobTemplate
|
|
serializer_class = serializers.JobTemplateSerializer
|
|
always_allow_superuser = False
|
|
|
|
def post(self, request, *args, **kwargs):
|
|
ret = super(JobTemplateList, self).post(request, *args, **kwargs)
|
|
if ret.status_code == 201:
|
|
job_template = models.JobTemplate.objects.get(id=ret.data['id'])
|
|
job_template.admin_role.members.add(request.user)
|
|
return ret
|
|
|
|
|
|
class JobTemplateDetail(RelatedJobsPreventDeleteMixin, RetrieveUpdateDestroyAPIView):
|
|
|
|
model = models.JobTemplate
|
|
serializer_class = serializers.JobTemplateSerializer
|
|
always_allow_superuser = False
|
|
|
|
|
|
class JobTemplateLaunch(RetrieveAPIView):
|
|
|
|
model = models.JobTemplate
|
|
obj_permission_type = 'start'
|
|
serializer_class = serializers.JobLaunchSerializer
|
|
always_allow_superuser = False
|
|
|
|
def update_raw_data(self, data):
|
|
try:
|
|
obj = self.get_object()
|
|
except PermissionDenied:
|
|
return data
|
|
extra_vars = data.pop('extra_vars', None) or {}
|
|
if obj:
|
|
needed_passwords = obj.passwords_needed_to_start
|
|
if needed_passwords:
|
|
data['credential_passwords'] = {}
|
|
for p in needed_passwords:
|
|
data['credential_passwords'][p] = u''
|
|
else:
|
|
data.pop('credential_passwords')
|
|
for v in obj.variables_needed_to_start:
|
|
extra_vars.setdefault(v, u'')
|
|
if extra_vars:
|
|
data['extra_vars'] = extra_vars
|
|
modified_ask_mapping = models.JobTemplate.get_ask_mapping()
|
|
modified_ask_mapping.pop('extra_vars')
|
|
for field, ask_field_name in modified_ask_mapping.items():
|
|
if not getattr(obj, ask_field_name):
|
|
data.pop(field, None)
|
|
elif field == 'inventory':
|
|
data[field] = getattrd(obj, "%s.%s" % (field, 'id'), None)
|
|
elif field == 'credentials':
|
|
data[field] = [cred.id for cred in obj.credentials.all()]
|
|
else:
|
|
data[field] = getattr(obj, field)
|
|
return data
|
|
|
|
def modernize_launch_payload(self, data, obj):
|
|
'''
|
|
Steps to do simple translations of request data to support
|
|
old field structure to launch endpoint
|
|
TODO: delete this method with future API version changes
|
|
'''
|
|
modern_data = data.copy()
|
|
|
|
id_fd = '{}_id'.format('inventory')
|
|
if 'inventory' not in modern_data and id_fd in modern_data:
|
|
modern_data['inventory'] = modern_data[id_fd]
|
|
|
|
# credential passwords were historically provided as top-level attributes
|
|
if 'credential_passwords' not in modern_data:
|
|
modern_data['credential_passwords'] = data.copy()
|
|
|
|
return modern_data
|
|
|
|
|
|
def post(self, request, *args, **kwargs):
|
|
obj = self.get_object()
|
|
|
|
try:
|
|
modern_data = self.modernize_launch_payload(
|
|
data=request.data, obj=obj
|
|
)
|
|
except ParseError as exc:
|
|
return Response(exc.detail, status=status.HTTP_400_BAD_REQUEST)
|
|
|
|
serializer = self.serializer_class(data=modern_data, context={'template': obj})
|
|
if not serializer.is_valid():
|
|
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
|
|
|
if not request.user.can_access(models.JobLaunchConfig, 'add', serializer.validated_data, template=obj):
|
|
raise PermissionDenied()
|
|
|
|
passwords = serializer.validated_data.pop('credential_passwords', {})
|
|
new_job = obj.create_unified_job(**serializer.validated_data)
|
|
result = new_job.signal_start(**passwords)
|
|
|
|
if not result:
|
|
data = dict(passwords_needed_to_start=new_job.passwords_needed_to_start)
|
|
new_job.delete()
|
|
return Response(data, status=status.HTTP_400_BAD_REQUEST)
|
|
else:
|
|
data = OrderedDict()
|
|
if isinstance(new_job, models.WorkflowJob):
|
|
data['workflow_job'] = new_job.id
|
|
data['ignored_fields'] = self.sanitize_for_response(serializer._ignored_fields)
|
|
data.update(serializers.WorkflowJobSerializer(new_job, context=self.get_serializer_context()).to_representation(new_job))
|
|
else:
|
|
data['job'] = new_job.id
|
|
data['ignored_fields'] = self.sanitize_for_response(serializer._ignored_fields)
|
|
data.update(serializers.JobSerializer(new_job, context=self.get_serializer_context()).to_representation(new_job))
|
|
headers = {'Location': new_job.get_absolute_url(request)}
|
|
return Response(data, status=status.HTTP_201_CREATED, headers=headers)
|
|
|
|
|
|
def sanitize_for_response(self, data):
|
|
'''
|
|
Model objects cannot be serialized by DRF,
|
|
this replaces objects with their ids for inclusion in response
|
|
'''
|
|
|
|
def display_value(val):
|
|
if hasattr(val, 'id'):
|
|
return val.id
|
|
else:
|
|
return val
|
|
|
|
sanitized_data = {}
|
|
for field_name, value in data.items():
|
|
if isinstance(value, (set, list)):
|
|
sanitized_data[field_name] = []
|
|
for sub_value in value:
|
|
sanitized_data[field_name].append(display_value(sub_value))
|
|
else:
|
|
sanitized_data[field_name] = display_value(value)
|
|
|
|
return sanitized_data
|
|
|
|
|
|
class JobTemplateSchedulesList(SubListCreateAPIView):
|
|
|
|
name = _("Job Template Schedules")
|
|
|
|
model = models.Schedule
|
|
serializer_class = serializers.ScheduleSerializer
|
|
parent_model = models.JobTemplate
|
|
relationship = 'schedules'
|
|
parent_key = 'unified_job_template'
|
|
|
|
|
|
class JobTemplateSurveySpec(GenericAPIView):
|
|
|
|
model = models.JobTemplate
|
|
obj_permission_type = 'admin'
|
|
serializer_class = serializers.EmptySerializer
|
|
|
|
ALLOWED_TYPES = {
|
|
'text': str,
|
|
'textarea': str,
|
|
'password': str,
|
|
'multiplechoice': str,
|
|
'multiselect': str,
|
|
'integer': int,
|
|
'float': float
|
|
}
|
|
|
|
def get(self, request, *args, **kwargs):
|
|
obj = self.get_object()
|
|
return Response(obj.display_survey_spec())
|
|
|
|
def post(self, request, *args, **kwargs):
|
|
obj = self.get_object()
|
|
|
|
if not request.user.can_access(self.model, 'change', obj, None):
|
|
raise PermissionDenied()
|
|
response = self._validate_spec_data(request.data, obj.survey_spec)
|
|
if response:
|
|
return response
|
|
obj.survey_spec = request.data
|
|
obj.save(update_fields=['survey_spec'])
|
|
return Response()
|
|
|
|
@staticmethod
|
|
def _validate_spec_data(new_spec, old_spec):
|
|
schema_errors = {}
|
|
for field, expect_type, type_label in [
|
|
('name', str, 'string'),
|
|
('description', str, 'string'),
|
|
('spec', list, 'list of items')]:
|
|
if field not in new_spec:
|
|
schema_errors['error'] = _("Field '{}' is missing from survey spec.").format(field)
|
|
elif not isinstance(new_spec[field], expect_type):
|
|
schema_errors['error'] = _("Expected {} for field '{}', received {} type.").format(
|
|
type_label, field, type(new_spec[field]).__name__)
|
|
|
|
if isinstance(new_spec.get('spec', None), list) and len(new_spec["spec"]) < 1:
|
|
schema_errors['error'] = _("'spec' doesn't contain any items.")
|
|
|
|
if schema_errors:
|
|
return Response(schema_errors, status=status.HTTP_400_BAD_REQUEST)
|
|
|
|
variable_set = set()
|
|
old_spec_dict = models.JobTemplate.pivot_spec(old_spec)
|
|
for idx, survey_item in enumerate(new_spec["spec"]):
|
|
context = dict(
|
|
idx=str(idx),
|
|
survey_item=survey_item
|
|
)
|
|
# General element validation
|
|
if not isinstance(survey_item, dict):
|
|
return Response(dict(error=_("Survey question %s is not a json object.") % str(idx)), status=status.HTTP_400_BAD_REQUEST)
|
|
for field_name in ['type', 'question_name', 'variable', 'required']:
|
|
if field_name not in survey_item:
|
|
return Response(dict(error=_("'{field_name}' missing from survey question {idx}").format(
|
|
field_name=field_name, **context
|
|
)), status=status.HTTP_400_BAD_REQUEST)
|
|
val = survey_item[field_name]
|
|
allow_types = str
|
|
type_label = 'string'
|
|
if field_name == 'required':
|
|
allow_types = bool
|
|
type_label = 'boolean'
|
|
if not isinstance(val, allow_types):
|
|
return Response(dict(error=_("'{field_name}' in survey question {idx} expected to be {type_label}.").format(
|
|
field_name=field_name, type_label=type_label, **context
|
|
)), status=status.HTTP_400_BAD_REQUEST)
|
|
if survey_item['variable'] in variable_set:
|
|
return Response(dict(error=_("'variable' '%(item)s' duplicated in survey question %(survey)s.") % {
|
|
'item': survey_item['variable'], 'survey': str(idx)}), status=status.HTTP_400_BAD_REQUEST)
|
|
else:
|
|
variable_set.add(survey_item['variable'])
|
|
|
|
# Type-specific validation
|
|
# validate question type <-> default type
|
|
qtype = survey_item["type"]
|
|
if qtype not in JobTemplateSurveySpec.ALLOWED_TYPES:
|
|
return Response(dict(error=_(
|
|
"'{survey_item[type]}' in survey question {idx} is not one of '{allowed_types}' allowed question types."
|
|
).format(
|
|
allowed_types=', '.join(JobTemplateSurveySpec.ALLOWED_TYPES.keys()), **context
|
|
)), status=status.HTTP_400_BAD_REQUEST)
|
|
if 'default' in survey_item and survey_item['default'] != '':
|
|
if not isinstance(survey_item['default'], JobTemplateSurveySpec.ALLOWED_TYPES[qtype]):
|
|
type_label = 'string'
|
|
if qtype in ['integer', 'float']:
|
|
type_label = qtype
|
|
return Response(dict(error=_(
|
|
"Default value {survey_item[default]} in survey question {idx} expected to be {type_label}."
|
|
).format(
|
|
type_label=type_label, **context
|
|
)), status=status.HTTP_400_BAD_REQUEST)
|
|
# additional type-specific properties, the UI provides these even
|
|
# if not applicable to the question, TODO: request that they not do this
|
|
for key in ['min', 'max']:
|
|
if key in survey_item:
|
|
if survey_item[key] is not None and (not isinstance(survey_item[key], int)):
|
|
return Response(dict(error=_(
|
|
"The {min_or_max} limit in survey question {idx} expected to be integer."
|
|
).format(min_or_max=key, **context)), status=status.HTTP_400_BAD_REQUEST)
|
|
# if it's a multiselect or multiple choice, it must have coices listed
|
|
# choices and defualts must come in as strings seperated by /n characters.
|
|
if qtype == 'multiselect' or qtype == 'multiplechoice':
|
|
if 'choices' in survey_item:
|
|
if isinstance(survey_item['choices'], str):
|
|
survey_item['choices'] = '\n'.join(choice for choice in survey_item['choices'].splitlines() if choice.strip() != '')
|
|
else:
|
|
return Response(dict(error=_(
|
|
"Survey question {idx} of type {survey_item[type]} must specify choices.".format(**context)
|
|
)), status=status.HTTP_400_BAD_REQUEST)
|
|
# If there is a default string split it out removing extra /n characters.
|
|
# Note: There can still be extra newline characters added in the API, these are sanitized out using .strip()
|
|
if 'default' in survey_item:
|
|
if isinstance(survey_item['default'], str):
|
|
survey_item['default'] = '\n'.join(choice for choice in survey_item['default'].splitlines() if choice.strip() != '')
|
|
list_of_defaults = survey_item['default'].splitlines()
|
|
else:
|
|
list_of_defaults = survey_item['default']
|
|
if qtype == 'multiplechoice':
|
|
# Multiplechoice types should only have 1 default.
|
|
if len(list_of_defaults) > 1:
|
|
return Response(dict(error=_(
|
|
"Multiple Choice (Single Select) can only have one default value.".format(**context)
|
|
)), status=status.HTTP_400_BAD_REQUEST)
|
|
if any(item not in survey_item['choices'] for item in list_of_defaults):
|
|
return Response(dict(error=_(
|
|
"Default choice must be answered from the choices listed.".format(**context)
|
|
)), status=status.HTTP_400_BAD_REQUEST)
|
|
|
|
# Process encryption substitution
|
|
if ("default" in survey_item and isinstance(survey_item['default'], str) and
|
|
survey_item['default'].startswith('$encrypted$')):
|
|
# Submission expects the existence of encrypted DB value to replace given default
|
|
if qtype != "password":
|
|
return Response(dict(error=_(
|
|
"$encrypted$ is a reserved keyword for password question defaults, "
|
|
"survey question {idx} is type {survey_item[type]}."
|
|
).format(**context)), status=status.HTTP_400_BAD_REQUEST)
|
|
old_element = old_spec_dict.get(survey_item['variable'], {})
|
|
encryptedish_default_exists = False
|
|
if 'default' in old_element:
|
|
old_default = old_element['default']
|
|
if isinstance(old_default, str):
|
|
if old_default.startswith('$encrypted$'):
|
|
encryptedish_default_exists = True
|
|
elif old_default == "": # unencrypted blank string is allowed as DB value as special case
|
|
encryptedish_default_exists = True
|
|
if not encryptedish_default_exists:
|
|
return Response(dict(error=_(
|
|
"$encrypted$ is a reserved keyword, may not be used for new default in position {idx}."
|
|
).format(**context)), status=status.HTTP_400_BAD_REQUEST)
|
|
survey_item['default'] = old_element['default']
|
|
elif qtype == "password" and 'default' in survey_item:
|
|
# Submission provides new encrypted default
|
|
survey_item['default'] = encrypt_value(survey_item['default'])
|
|
|
|
def delete(self, request, *args, **kwargs):
|
|
obj = self.get_object()
|
|
if not request.user.can_access(self.model, 'delete', obj):
|
|
raise PermissionDenied()
|
|
obj.survey_spec = {}
|
|
obj.save()
|
|
return Response()
|
|
|
|
|
|
class WorkflowJobTemplateSurveySpec(JobTemplateSurveySpec):
|
|
|
|
model = models.WorkflowJobTemplate
|
|
|
|
|
|
class JobTemplateActivityStreamList(SubListAPIView):
|
|
|
|
model = models.ActivityStream
|
|
serializer_class = serializers.ActivityStreamSerializer
|
|
parent_model = models.JobTemplate
|
|
relationship = 'activitystream_set'
|
|
search_fields = ('changes',)
|
|
|
|
|
|
class JobTemplateNotificationTemplatesAnyList(SubListCreateAttachDetachAPIView):
|
|
|
|
model = models.NotificationTemplate
|
|
serializer_class = serializers.NotificationTemplateSerializer
|
|
parent_model = models.JobTemplate
|
|
|
|
|
|
class JobTemplateNotificationTemplatesStartedList(JobTemplateNotificationTemplatesAnyList):
|
|
|
|
relationship = 'notification_templates_started'
|
|
|
|
|
|
class JobTemplateNotificationTemplatesErrorList(JobTemplateNotificationTemplatesAnyList):
|
|
|
|
relationship = 'notification_templates_error'
|
|
|
|
|
|
class JobTemplateNotificationTemplatesSuccessList(JobTemplateNotificationTemplatesAnyList):
|
|
|
|
relationship = 'notification_templates_success'
|
|
|
|
|
|
class JobTemplateCredentialsList(SubListCreateAttachDetachAPIView):
|
|
|
|
model = models.Credential
|
|
serializer_class = serializers.CredentialSerializer
|
|
parent_model = models.JobTemplate
|
|
relationship = 'credentials'
|
|
|
|
def get_queryset(self):
|
|
# Return the full list of credentials
|
|
parent = self.get_parent_object()
|
|
self.check_parent_access(parent)
|
|
sublist_qs = getattrd(parent, self.relationship)
|
|
sublist_qs = sublist_qs.prefetch_related(
|
|
'created_by', 'modified_by',
|
|
'admin_role', 'use_role', 'read_role',
|
|
'admin_role__parents', 'admin_role__members')
|
|
return sublist_qs
|
|
|
|
def is_valid_relation(self, parent, sub, created=False):
|
|
if sub.unique_hash() in [cred.unique_hash() for cred in parent.credentials.all()]:
|
|
return {"error": _("Cannot assign multiple {credential_type} credentials.").format(
|
|
credential_type=sub.unique_hash(display=True))}
|
|
kind = sub.credential_type.kind
|
|
if kind not in ('ssh', 'vault', 'cloud', 'net', 'kubernetes'):
|
|
return {'error': _('Cannot assign a Credential of kind `{}`.').format(kind)}
|
|
|
|
return super(JobTemplateCredentialsList, self).is_valid_relation(parent, sub, created)
|
|
|
|
|
|
class JobTemplateLabelList(DeleteLastUnattachLabelMixin, SubListCreateAttachDetachAPIView):
|
|
|
|
model = models.Label
|
|
serializer_class = serializers.LabelSerializer
|
|
parent_model = models.JobTemplate
|
|
relationship = 'labels'
|
|
|
|
def post(self, request, *args, **kwargs):
|
|
# If a label already exists in the database, attach it instead of erroring out
|
|
# that it already exists
|
|
if 'id' not in request.data and 'name' in request.data and 'organization' in request.data:
|
|
existing = models.Label.objects.filter(name=request.data['name'], organization_id=request.data['organization'])
|
|
if existing.exists():
|
|
existing = existing[0]
|
|
request.data['id'] = existing.id
|
|
del request.data['name']
|
|
del request.data['organization']
|
|
if models.Label.objects.filter(unifiedjobtemplate_labels=self.kwargs['pk']).count() > 100:
|
|
return Response(dict(msg=_('Maximum number of labels for {} reached.'.format(
|
|
self.parent_model._meta.verbose_name_raw))), status=status.HTTP_400_BAD_REQUEST)
|
|
return super(JobTemplateLabelList, self).post(request, *args, **kwargs)
|
|
|
|
|
|
class JobTemplateCallback(GenericAPIView):
|
|
|
|
model = models.JobTemplate
|
|
permission_classes = (JobTemplateCallbackPermission,)
|
|
serializer_class = serializers.EmptySerializer
|
|
parser_classes = api_settings.DEFAULT_PARSER_CLASSES + [FormParser]
|
|
|
|
@csrf_exempt
|
|
@transaction.non_atomic_requests
|
|
def dispatch(self, *args, **kwargs):
|
|
return super(JobTemplateCallback, self).dispatch(*args, **kwargs)
|
|
|
|
def find_matching_hosts(self):
|
|
'''
|
|
Find the host(s) in the job template's inventory that match the remote
|
|
host for the current request.
|
|
'''
|
|
# Find the list of remote host names/IPs to check.
|
|
remote_hosts = set()
|
|
for header in settings.REMOTE_HOST_HEADERS:
|
|
for value in self.request.META.get(header, '').split(','):
|
|
value = value.strip()
|
|
if value:
|
|
remote_hosts.add(value)
|
|
# Add the reverse lookup of IP addresses.
|
|
for rh in list(remote_hosts):
|
|
try:
|
|
result = socket.gethostbyaddr(rh)
|
|
except socket.herror:
|
|
continue
|
|
except socket.gaierror:
|
|
continue
|
|
remote_hosts.add(result[0])
|
|
remote_hosts.update(result[1])
|
|
# Filter out any .arpa results.
|
|
for rh in list(remote_hosts):
|
|
if rh.endswith('.arpa'):
|
|
remote_hosts.remove(rh)
|
|
if not remote_hosts:
|
|
return set()
|
|
# Find the host objects to search for a match.
|
|
obj = self.get_object()
|
|
hosts = obj.inventory.hosts.all()
|
|
# Populate host_mappings
|
|
host_mappings = {}
|
|
for host in hosts:
|
|
host_name = host.get_effective_host_name()
|
|
host_mappings.setdefault(host_name, [])
|
|
host_mappings[host_name].append(host)
|
|
# Try finding direct match
|
|
matches = set()
|
|
for host_name in remote_hosts:
|
|
if host_name in host_mappings:
|
|
matches.update(host_mappings[host_name])
|
|
if len(matches) == 1:
|
|
return matches
|
|
# Try to resolve forward addresses for each host to find matches.
|
|
for host_name in host_mappings:
|
|
try:
|
|
result = socket.getaddrinfo(host_name, None)
|
|
possible_ips = set(x[4][0] for x in result)
|
|
possible_ips.discard(host_name)
|
|
if possible_ips and possible_ips & remote_hosts:
|
|
matches.update(host_mappings[host_name])
|
|
except socket.gaierror:
|
|
pass
|
|
except UnicodeError:
|
|
pass
|
|
return matches
|
|
|
|
def get(self, request, *args, **kwargs):
|
|
job_template = self.get_object()
|
|
matching_hosts = self.find_matching_hosts()
|
|
data = dict(
|
|
host_config_key=job_template.host_config_key,
|
|
matching_hosts=[x.name for x in matching_hosts],
|
|
)
|
|
if settings.DEBUG:
|
|
d = dict([(k,v) for k,v in request.META.items()
|
|
if k.startswith('HTTP_') or k.startswith('REMOTE_')])
|
|
data['request_meta'] = d
|
|
return Response(data)
|
|
|
|
def post(self, request, *args, **kwargs):
|
|
extra_vars = None
|
|
# Be careful here: content_type can look like '<content_type>; charset=blar'
|
|
if request.content_type.startswith("application/json"):
|
|
extra_vars = request.data.get("extra_vars", None)
|
|
# Permission class should have already validated host_config_key.
|
|
job_template = self.get_object()
|
|
# Attempt to find matching hosts based on remote address.
|
|
matching_hosts = self.find_matching_hosts()
|
|
# If the host is not found, update the inventory before trying to
|
|
# match again.
|
|
inventory_sources_already_updated = []
|
|
if len(matching_hosts) != 1:
|
|
inventory_sources = job_template.inventory.inventory_sources.filter( update_on_launch=True)
|
|
inventory_update_pks = set()
|
|
for inventory_source in inventory_sources:
|
|
if inventory_source.needs_update_on_launch:
|
|
# FIXME: Doesn't check for any existing updates.
|
|
inventory_update = inventory_source.create_inventory_update(
|
|
**{'_eager_fields': {'launch_type': 'callback'}}
|
|
)
|
|
inventory_update.signal_start()
|
|
inventory_update_pks.add(inventory_update.pk)
|
|
inventory_update_qs = models.InventoryUpdate.objects.filter(pk__in=inventory_update_pks, status__in=('pending', 'waiting', 'running'))
|
|
# Poll for the inventory updates we've started to complete.
|
|
while inventory_update_qs.count():
|
|
time.sleep(1.0)
|
|
transaction.commit()
|
|
# Ignore failed inventory updates here, only add successful ones
|
|
# to the list to be excluded when running the job.
|
|
for inventory_update in models.InventoryUpdate.objects.filter(pk__in=inventory_update_pks, status='successful'):
|
|
inventory_sources_already_updated.append(inventory_update.inventory_source_id)
|
|
matching_hosts = self.find_matching_hosts()
|
|
# Check matching hosts.
|
|
if not matching_hosts:
|
|
data = dict(msg=_('No matching host could be found!'))
|
|
return Response(data, status=status.HTTP_400_BAD_REQUEST)
|
|
elif len(matching_hosts) > 1:
|
|
data = dict(msg=_('Multiple hosts matched the request!'))
|
|
return Response(data, status=status.HTTP_400_BAD_REQUEST)
|
|
else:
|
|
host = list(matching_hosts)[0]
|
|
if not job_template.can_start_without_user_input(callback_extra_vars=extra_vars):
|
|
data = dict(msg=_('Cannot start automatically, user input required!'))
|
|
return Response(data, status=status.HTTP_400_BAD_REQUEST)
|
|
limit = host.name
|
|
|
|
# NOTE: We limit this to one job waiting per host per callblack to keep them from stacking crazily
|
|
if models.Job.objects.filter(
|
|
status__in=['pending', 'waiting', 'running'], job_template=job_template, limit=limit
|
|
).count() > 0:
|
|
data = dict(msg=_('Host callback job already pending.'))
|
|
return Response(data, status=status.HTTP_400_BAD_REQUEST)
|
|
|
|
# Everything is fine; actually create the job.
|
|
kv = {"limit": limit}
|
|
kv.setdefault('_eager_fields', {})['launch_type'] = 'callback'
|
|
if extra_vars is not None and job_template.ask_variables_on_launch:
|
|
extra_vars_redacted, removed = extract_ansible_vars(extra_vars)
|
|
kv['extra_vars'] = extra_vars_redacted
|
|
kv['_prevent_slicing'] = True # will only run against 1 host, so no point
|
|
with transaction.atomic():
|
|
job = job_template.create_job(**kv)
|
|
|
|
# Send a signal to signify that the job should be started.
|
|
result = job.signal_start(inventory_sources_already_updated=inventory_sources_already_updated)
|
|
if not result:
|
|
data = dict(msg=_('Error starting job!'))
|
|
job.delete()
|
|
return Response(data, status=status.HTTP_400_BAD_REQUEST)
|
|
|
|
# Return the location of the new job.
|
|
headers = {'Location': job.get_absolute_url(request=request)}
|
|
return Response(status=status.HTTP_201_CREATED, headers=headers)
|
|
|
|
|
|
class JobTemplateJobsList(SubListAPIView):
|
|
|
|
model = models.Job
|
|
serializer_class = serializers.JobListSerializer
|
|
parent_model = models.JobTemplate
|
|
relationship = 'jobs'
|
|
parent_key = 'job_template'
|
|
|
|
|
|
class JobTemplateSliceWorkflowJobsList(SubListCreateAPIView):
|
|
|
|
model = models.WorkflowJob
|
|
serializer_class = serializers.WorkflowJobListSerializer
|
|
parent_model = models.JobTemplate
|
|
relationship = 'slice_workflow_jobs'
|
|
parent_key = 'job_template'
|
|
|
|
|
|
class JobTemplateInstanceGroupsList(SubListAttachDetachAPIView):
|
|
|
|
model = models.InstanceGroup
|
|
serializer_class = serializers.InstanceGroupSerializer
|
|
parent_model = models.JobTemplate
|
|
relationship = 'instance_groups'
|
|
|
|
|
|
class JobTemplateAccessList(ResourceAccessList):
|
|
|
|
model = models.User # needs to be User for AccessLists's
|
|
parent_model = models.JobTemplate
|
|
|
|
|
|
class JobTemplateObjectRolesList(SubListAPIView):
|
|
|
|
model = models.Role
|
|
serializer_class = serializers.RoleSerializer
|
|
parent_model = models.JobTemplate
|
|
search_fields = ('role_field', 'content_type__model',)
|
|
|
|
def get_queryset(self):
|
|
po = self.get_parent_object()
|
|
content_type = ContentType.objects.get_for_model(self.parent_model)
|
|
return models.Role.objects.filter(content_type=content_type, object_id=po.pk)
|
|
|
|
|
|
class JobTemplateCopy(CopyAPIView):
|
|
|
|
model = models.JobTemplate
|
|
copy_return_serializer_class = serializers.JobTemplateSerializer
|
|
|
|
|
|
class WorkflowJobNodeList(ListAPIView):
|
|
|
|
model = models.WorkflowJobNode
|
|
serializer_class = serializers.WorkflowJobNodeListSerializer
|
|
search_fields = ('unified_job_template__name', 'unified_job_template__description',)
|
|
|
|
|
|
class WorkflowJobNodeDetail(RetrieveAPIView):
|
|
|
|
model = models.WorkflowJobNode
|
|
serializer_class = serializers.WorkflowJobNodeDetailSerializer
|
|
|
|
|
|
class WorkflowJobNodeCredentialsList(SubListAPIView):
|
|
|
|
model = models.Credential
|
|
serializer_class = serializers.CredentialSerializer
|
|
parent_model = models.WorkflowJobNode
|
|
relationship = 'credentials'
|
|
|
|
|
|
class WorkflowJobTemplateNodeList(ListCreateAPIView):
|
|
|
|
model = models.WorkflowJobTemplateNode
|
|
serializer_class = serializers.WorkflowJobTemplateNodeSerializer
|
|
search_fields = ('unified_job_template__name', 'unified_job_template__description',)
|
|
|
|
|
|
class WorkflowJobTemplateNodeDetail(RetrieveUpdateDestroyAPIView):
|
|
|
|
model = models.WorkflowJobTemplateNode
|
|
serializer_class = serializers.WorkflowJobTemplateNodeDetailSerializer
|
|
|
|
|
|
class WorkflowJobTemplateNodeCredentialsList(LaunchConfigCredentialsBase):
|
|
|
|
parent_model = models.WorkflowJobTemplateNode
|
|
|
|
|
|
class WorkflowJobTemplateNodeChildrenBaseList(EnforceParentRelationshipMixin, SubListCreateAttachDetachAPIView):
|
|
|
|
model = models.WorkflowJobTemplateNode
|
|
serializer_class = serializers.WorkflowJobTemplateNodeSerializer
|
|
always_allow_superuser = True
|
|
parent_model = models.WorkflowJobTemplateNode
|
|
relationship = ''
|
|
enforce_parent_relationship = 'workflow_job_template'
|
|
search_fields = ('unified_job_template__name', 'unified_job_template__description',)
|
|
|
|
'''
|
|
Limit the set of WorkflowJobTemplateNodes to the related nodes of specified by
|
|
'relationship'
|
|
'''
|
|
def get_queryset(self):
|
|
parent = self.get_parent_object()
|
|
self.check_parent_access(parent)
|
|
return getattr(parent, self.relationship).all()
|
|
|
|
def is_valid_relation(self, parent, sub, created=False):
|
|
|
|
if created:
|
|
return None
|
|
|
|
if parent.id == sub.id:
|
|
return {"Error": _("Cycle detected.")}
|
|
|
|
'''
|
|
Look for parent->child connection in all relationships except the relationship that is
|
|
attempting to be added; because it's ok to re-add the relationship
|
|
'''
|
|
relationships = ['success_nodes', 'failure_nodes', 'always_nodes']
|
|
relationships.remove(self.relationship)
|
|
qs = functools.reduce(lambda x, y: (x | y),
|
|
(Q(**{'{}__in'.format(r): [sub.id]}) for r in relationships))
|
|
|
|
if models.WorkflowJobTemplateNode.objects.filter(Q(pk=parent.id) & qs).exists():
|
|
return {"Error": _("Relationship not allowed.")}
|
|
|
|
parent_node_type_relationship = getattr(parent, self.relationship)
|
|
parent_node_type_relationship.add(sub)
|
|
|
|
graph = WorkflowDAG(parent.workflow_job_template)
|
|
if graph.has_cycle():
|
|
parent_node_type_relationship.remove(sub)
|
|
return {"Error": _("Cycle detected.")}
|
|
parent_node_type_relationship.remove(sub)
|
|
return None
|
|
|
|
|
|
class WorkflowJobTemplateNodeCreateApproval(RetrieveAPIView):
|
|
|
|
model = models.WorkflowJobTemplateNode
|
|
serializer_class = serializers.WorkflowJobTemplateNodeCreateApprovalSerializer
|
|
permission_classes = []
|
|
|
|
def post(self, request, *args, **kwargs):
|
|
obj = self.get_object()
|
|
serializer = self.get_serializer(instance=obj, data=request.data)
|
|
if not serializer.is_valid():
|
|
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
|
approval_template = obj.create_approval_template(**serializer.validated_data)
|
|
data = serializers.WorkflowApprovalTemplateSerializer(
|
|
approval_template,
|
|
context=self.get_serializer_context()
|
|
).data
|
|
return Response(data, status=status.HTTP_201_CREATED)
|
|
|
|
def check_permissions(self, request):
|
|
obj = self.get_object().workflow_job_template
|
|
if request.method == 'POST':
|
|
if not request.user.can_access(models.WorkflowJobTemplate, 'change', obj, request.data):
|
|
self.permission_denied(request)
|
|
else:
|
|
if not request.user.can_access(models.WorkflowJobTemplate, 'read', obj):
|
|
self.permission_denied(request)
|
|
|
|
|
|
class WorkflowJobTemplateNodeSuccessNodesList(WorkflowJobTemplateNodeChildrenBaseList):
|
|
relationship = 'success_nodes'
|
|
|
|
|
|
class WorkflowJobTemplateNodeFailureNodesList(WorkflowJobTemplateNodeChildrenBaseList):
|
|
relationship = 'failure_nodes'
|
|
|
|
|
|
class WorkflowJobTemplateNodeAlwaysNodesList(WorkflowJobTemplateNodeChildrenBaseList):
|
|
relationship = 'always_nodes'
|
|
|
|
|
|
class WorkflowJobNodeChildrenBaseList(SubListAPIView):
|
|
|
|
model = models.WorkflowJobNode
|
|
serializer_class = serializers.WorkflowJobNodeListSerializer
|
|
parent_model = models.WorkflowJobNode
|
|
relationship = ''
|
|
search_fields = ('unified_job_template__name', 'unified_job_template__description',)
|
|
|
|
#
|
|
#Limit the set of WorkflowJobeNodes to the related nodes of specified by
|
|
#'relationship'
|
|
#
|
|
def get_queryset(self):
|
|
parent = self.get_parent_object()
|
|
self.check_parent_access(parent)
|
|
return getattr(parent, self.relationship).all()
|
|
|
|
|
|
class WorkflowJobNodeSuccessNodesList(WorkflowJobNodeChildrenBaseList):
|
|
relationship = 'success_nodes'
|
|
|
|
|
|
class WorkflowJobNodeFailureNodesList(WorkflowJobNodeChildrenBaseList):
|
|
relationship = 'failure_nodes'
|
|
|
|
|
|
class WorkflowJobNodeAlwaysNodesList(WorkflowJobNodeChildrenBaseList):
|
|
relationship = 'always_nodes'
|
|
|
|
|
|
class WorkflowJobTemplateList(ListCreateAPIView):
|
|
|
|
model = models.WorkflowJobTemplate
|
|
serializer_class = serializers.WorkflowJobTemplateSerializer
|
|
always_allow_superuser = False
|
|
|
|
|
|
class WorkflowJobTemplateDetail(RelatedJobsPreventDeleteMixin, RetrieveUpdateDestroyAPIView):
|
|
|
|
model = models.WorkflowJobTemplate
|
|
serializer_class = serializers.WorkflowJobTemplateSerializer
|
|
always_allow_superuser = False
|
|
|
|
|
|
class WorkflowJobTemplateCopy(CopyAPIView):
|
|
|
|
model = models.WorkflowJobTemplate
|
|
copy_return_serializer_class = serializers.WorkflowJobTemplateSerializer
|
|
|
|
def get(self, request, *args, **kwargs):
|
|
obj = self.get_object()
|
|
if not request.user.can_access(obj.__class__, 'read', obj):
|
|
raise PermissionDenied()
|
|
can_copy, messages = request.user.can_access_with_errors(self.model, 'copy', obj)
|
|
data = OrderedDict([
|
|
('can_copy', can_copy), ('can_copy_without_user_input', can_copy),
|
|
('templates_unable_to_copy', [] if can_copy else ['all']),
|
|
('credentials_unable_to_copy', [] if can_copy else ['all']),
|
|
('inventories_unable_to_copy', [] if can_copy else ['all'])
|
|
])
|
|
if messages and can_copy:
|
|
data['can_copy_without_user_input'] = False
|
|
data.update(messages)
|
|
return Response(data)
|
|
|
|
def _build_create_dict(self, obj):
|
|
"""Special processing of fields managed by char_prompts
|
|
"""
|
|
r = super(WorkflowJobTemplateCopy, self)._build_create_dict(obj)
|
|
field_names = set(f.name for f in obj._meta.get_fields())
|
|
for field_name, ask_field_name in obj.get_ask_mapping().items():
|
|
if field_name in r and field_name not in field_names:
|
|
r.setdefault('char_prompts', {})
|
|
r['char_prompts'][field_name] = r.pop(field_name)
|
|
return r
|
|
|
|
@staticmethod
|
|
def deep_copy_permission_check_func(user, new_objs):
|
|
for obj in new_objs:
|
|
for field_name in obj._get_workflow_job_field_names():
|
|
item = getattr(obj, field_name, None)
|
|
if item is None:
|
|
continue
|
|
elif field_name in ['inventory']:
|
|
if not user.can_access(item.__class__, 'use', item):
|
|
setattr(obj, field_name, None)
|
|
elif field_name in ['unified_job_template']:
|
|
if not user.can_access(item.__class__, 'start', item, validate_license=False):
|
|
setattr(obj, field_name, None)
|
|
elif field_name in ['credentials']:
|
|
for cred in item.all():
|
|
if not user.can_access(cred.__class__, 'use', cred):
|
|
logger.debug(
|
|
'Deep copy: removing {} from relationship due to permissions'.format(cred))
|
|
item.remove(cred.pk)
|
|
obj.save()
|
|
|
|
|
|
class WorkflowJobTemplateLabelList(JobTemplateLabelList):
|
|
parent_model = models.WorkflowJobTemplate
|
|
|
|
|
|
class WorkflowJobTemplateLaunch(RetrieveAPIView):
|
|
|
|
model = models.WorkflowJobTemplate
|
|
obj_permission_type = 'start'
|
|
serializer_class = serializers.WorkflowJobLaunchSerializer
|
|
always_allow_superuser = False
|
|
|
|
def update_raw_data(self, data):
|
|
try:
|
|
obj = self.get_object()
|
|
except PermissionDenied:
|
|
return data
|
|
extra_vars = data.pop('extra_vars', None) or {}
|
|
if obj:
|
|
for v in obj.variables_needed_to_start:
|
|
extra_vars.setdefault(v, u'')
|
|
if extra_vars:
|
|
data['extra_vars'] = extra_vars
|
|
modified_ask_mapping = models.WorkflowJobTemplate.get_ask_mapping()
|
|
modified_ask_mapping.pop('extra_vars')
|
|
for field_name, ask_field_name in obj.get_ask_mapping().items():
|
|
if not getattr(obj, ask_field_name):
|
|
data.pop(field_name, None)
|
|
elif field_name == 'inventory':
|
|
data[field_name] = getattrd(obj, "%s.%s" % (field_name, 'id'), None)
|
|
else:
|
|
data[field_name] = getattr(obj, field_name)
|
|
return data
|
|
|
|
def post(self, request, *args, **kwargs):
|
|
obj = self.get_object()
|
|
|
|
if 'inventory_id' in request.data:
|
|
request.data['inventory'] = request.data['inventory_id']
|
|
|
|
serializer = self.serializer_class(instance=obj, data=request.data)
|
|
if not serializer.is_valid():
|
|
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
|
|
|
if not request.user.can_access(models.JobLaunchConfig, 'add', serializer.validated_data, template=obj):
|
|
raise PermissionDenied()
|
|
|
|
new_job = obj.create_unified_job(**serializer.validated_data)
|
|
new_job.signal_start()
|
|
|
|
data = OrderedDict()
|
|
data['workflow_job'] = new_job.id
|
|
data['ignored_fields'] = serializer._ignored_fields
|
|
data.update(serializers.WorkflowJobSerializer(new_job, context=self.get_serializer_context()).to_representation(new_job))
|
|
headers = {'Location': new_job.get_absolute_url(request)}
|
|
return Response(data, status=status.HTTP_201_CREATED, headers=headers)
|
|
|
|
|
|
class WorkflowJobRelaunch(GenericAPIView):
|
|
|
|
model = models.WorkflowJob
|
|
obj_permission_type = 'start'
|
|
serializer_class = serializers.EmptySerializer
|
|
|
|
def check_object_permissions(self, request, obj):
|
|
if request.method == 'POST' and obj:
|
|
relaunch_perm, messages = request.user.can_access_with_errors(self.model, 'start', obj)
|
|
if not relaunch_perm and 'workflow_job_template' in messages:
|
|
self.permission_denied(request, message=messages['workflow_job_template'])
|
|
return super(WorkflowJobRelaunch, self).check_object_permissions(request, obj)
|
|
|
|
def get(self, request, *args, **kwargs):
|
|
return Response({})
|
|
|
|
def post(self, request, *args, **kwargs):
|
|
obj = self.get_object()
|
|
if obj.is_sliced_job:
|
|
jt = obj.job_template
|
|
if not jt:
|
|
raise ParseError(_('Cannot relaunch slice workflow job orphaned from job template.'))
|
|
elif not obj.inventory or min(obj.inventory.hosts.count(), jt.job_slice_count) != obj.workflow_nodes.count():
|
|
raise ParseError(_('Cannot relaunch sliced workflow job after slice count has changed.'))
|
|
new_workflow_job = obj.create_relaunch_workflow_job()
|
|
new_workflow_job.signal_start()
|
|
|
|
data = serializers.WorkflowJobSerializer(new_workflow_job, context=self.get_serializer_context()).data
|
|
headers = {'Location': new_workflow_job.get_absolute_url(request=request)}
|
|
return Response(data, status=status.HTTP_201_CREATED, headers=headers)
|
|
|
|
|
|
class WorkflowJobTemplateWorkflowNodesList(SubListCreateAPIView):
|
|
|
|
model = models.WorkflowJobTemplateNode
|
|
serializer_class = serializers.WorkflowJobTemplateNodeSerializer
|
|
parent_model = models.WorkflowJobTemplate
|
|
relationship = 'workflow_job_template_nodes'
|
|
parent_key = 'workflow_job_template'
|
|
search_fields = ('unified_job_template__name', 'unified_job_template__description',)
|
|
|
|
def get_queryset(self):
|
|
return super(WorkflowJobTemplateWorkflowNodesList, self).get_queryset().order_by('id')
|
|
|
|
|
|
class WorkflowJobTemplateJobsList(SubListAPIView):
|
|
|
|
model = models.WorkflowJob
|
|
serializer_class = serializers.WorkflowJobListSerializer
|
|
parent_model = models.WorkflowJobTemplate
|
|
relationship = 'workflow_jobs'
|
|
parent_key = 'workflow_job_template'
|
|
|
|
|
|
class WorkflowJobTemplateSchedulesList(SubListCreateAPIView):
|
|
|
|
name = _("Workflow Job Template Schedules")
|
|
|
|
model = models.Schedule
|
|
serializer_class = serializers.ScheduleSerializer
|
|
parent_model = models.WorkflowJobTemplate
|
|
relationship = 'schedules'
|
|
parent_key = 'unified_job_template'
|
|
|
|
|
|
class WorkflowJobTemplateNotificationTemplatesAnyList(SubListCreateAttachDetachAPIView):
|
|
|
|
model = models.NotificationTemplate
|
|
serializer_class = serializers.NotificationTemplateSerializer
|
|
parent_model = models.WorkflowJobTemplate
|
|
|
|
|
|
class WorkflowJobTemplateNotificationTemplatesStartedList(WorkflowJobTemplateNotificationTemplatesAnyList):
|
|
|
|
relationship = 'notification_templates_started'
|
|
|
|
|
|
class WorkflowJobTemplateNotificationTemplatesErrorList(WorkflowJobTemplateNotificationTemplatesAnyList):
|
|
|
|
relationship = 'notification_templates_error'
|
|
|
|
|
|
class WorkflowJobTemplateNotificationTemplatesSuccessList(WorkflowJobTemplateNotificationTemplatesAnyList):
|
|
|
|
relationship = 'notification_templates_success'
|
|
|
|
|
|
class WorkflowJobTemplateNotificationTemplatesApprovalList(WorkflowJobTemplateNotificationTemplatesAnyList):
|
|
|
|
relationship = 'notification_templates_approvals'
|
|
|
|
|
|
class WorkflowJobTemplateAccessList(ResourceAccessList):
|
|
|
|
model = models.User # needs to be User for AccessLists's
|
|
parent_model = models.WorkflowJobTemplate
|
|
|
|
|
|
class WorkflowJobTemplateObjectRolesList(SubListAPIView):
|
|
|
|
model = models.Role
|
|
serializer_class = serializers.RoleSerializer
|
|
parent_model = models.WorkflowJobTemplate
|
|
search_fields = ('role_field', 'content_type__model',)
|
|
|
|
def get_queryset(self):
|
|
po = self.get_parent_object()
|
|
content_type = ContentType.objects.get_for_model(self.parent_model)
|
|
return models.Role.objects.filter(content_type=content_type, object_id=po.pk)
|
|
|
|
|
|
class WorkflowJobTemplateActivityStreamList(SubListAPIView):
|
|
|
|
model = models.ActivityStream
|
|
serializer_class = serializers.ActivityStreamSerializer
|
|
parent_model = models.WorkflowJobTemplate
|
|
relationship = 'activitystream_set'
|
|
search_fields = ('changes',)
|
|
|
|
def get_queryset(self):
|
|
parent = self.get_parent_object()
|
|
self.check_parent_access(parent)
|
|
qs = self.request.user.get_queryset(self.model)
|
|
return qs.filter(Q(workflow_job_template=parent) |
|
|
Q(workflow_job_template_node__workflow_job_template=parent)).distinct()
|
|
|
|
|
|
class WorkflowJobList(ListAPIView):
|
|
|
|
model = models.WorkflowJob
|
|
serializer_class = serializers.WorkflowJobListSerializer
|
|
|
|
|
|
class WorkflowJobDetail(UnifiedJobDeletionMixin, RetrieveDestroyAPIView):
|
|
|
|
model = models.WorkflowJob
|
|
serializer_class = serializers.WorkflowJobSerializer
|
|
|
|
|
|
class WorkflowJobWorkflowNodesList(SubListAPIView):
|
|
|
|
model = models.WorkflowJobNode
|
|
serializer_class = serializers.WorkflowJobNodeListSerializer
|
|
always_allow_superuser = True
|
|
parent_model = models.WorkflowJob
|
|
relationship = 'workflow_job_nodes'
|
|
parent_key = 'workflow_job'
|
|
search_fields = ('unified_job_template__name', 'unified_job_template__description',)
|
|
|
|
def get_queryset(self):
|
|
return super(WorkflowJobWorkflowNodesList, self).get_queryset().order_by('id')
|
|
|
|
|
|
class WorkflowJobCancel(RetrieveAPIView):
|
|
|
|
model = models.WorkflowJob
|
|
obj_permission_type = 'cancel'
|
|
serializer_class = serializers.WorkflowJobCancelSerializer
|
|
|
|
def post(self, request, *args, **kwargs):
|
|
obj = self.get_object()
|
|
if obj.can_cancel:
|
|
obj.cancel()
|
|
schedule_task_manager()
|
|
return Response(status=status.HTTP_202_ACCEPTED)
|
|
else:
|
|
return self.http_method_not_allowed(request, *args, **kwargs)
|
|
|
|
|
|
class WorkflowJobNotificationsList(SubListAPIView):
|
|
|
|
model = models.Notification
|
|
serializer_class = serializers.NotificationSerializer
|
|
parent_model = models.WorkflowJob
|
|
relationship = 'notifications'
|
|
search_fields = ('subject', 'notification_type', 'body',)
|
|
|
|
def get_sublist_queryset(self, parent):
|
|
return self.model.objects.filter(Q(unifiedjob_notifications=parent) |
|
|
Q(unifiedjob_notifications__unified_job_node__workflow_job=parent,
|
|
unifiedjob_notifications__workflowapproval__isnull=False)).distinct()
|
|
|
|
|
|
class WorkflowJobActivityStreamList(SubListAPIView):
|
|
|
|
model = models.ActivityStream
|
|
serializer_class = serializers.ActivityStreamSerializer
|
|
parent_model = models.WorkflowJob
|
|
relationship = 'activitystream_set'
|
|
search_fields = ('changes',)
|
|
|
|
|
|
class SystemJobTemplateList(ListAPIView):
|
|
|
|
model = models.SystemJobTemplate
|
|
serializer_class = serializers.SystemJobTemplateSerializer
|
|
|
|
def get(self, request, *args, **kwargs):
|
|
if not request.user.is_superuser and not request.user.is_system_auditor:
|
|
raise PermissionDenied(_("Superuser privileges needed."))
|
|
return super(SystemJobTemplateList, self).get(request, *args, **kwargs)
|
|
|
|
|
|
class SystemJobTemplateDetail(RetrieveAPIView):
|
|
|
|
model = models.SystemJobTemplate
|
|
serializer_class = serializers.SystemJobTemplateSerializer
|
|
|
|
|
|
class SystemJobTemplateLaunch(GenericAPIView):
|
|
|
|
model = models.SystemJobTemplate
|
|
obj_permission_type = 'start'
|
|
serializer_class = serializers.EmptySerializer
|
|
|
|
def get(self, request, *args, **kwargs):
|
|
return Response({})
|
|
|
|
def post(self, request, *args, **kwargs):
|
|
obj = self.get_object()
|
|
|
|
new_job = obj.create_unified_job(extra_vars=request.data.get('extra_vars', {}))
|
|
new_job.signal_start()
|
|
data = OrderedDict()
|
|
data['system_job'] = new_job.id
|
|
data.update(serializers.SystemJobSerializer(new_job, context=self.get_serializer_context()).to_representation(new_job))
|
|
headers = {'Location': new_job.get_absolute_url(request)}
|
|
return Response(data, status=status.HTTP_201_CREATED, headers=headers)
|
|
|
|
|
|
class SystemJobTemplateSchedulesList(SubListCreateAPIView):
|
|
|
|
name = _("System Job Template Schedules")
|
|
|
|
model = models.Schedule
|
|
serializer_class = serializers.ScheduleSerializer
|
|
parent_model = models.SystemJobTemplate
|
|
relationship = 'schedules'
|
|
parent_key = 'unified_job_template'
|
|
|
|
|
|
class SystemJobTemplateJobsList(SubListAPIView):
|
|
|
|
model = models.SystemJob
|
|
serializer_class = serializers.SystemJobListSerializer
|
|
parent_model = models.SystemJobTemplate
|
|
relationship = 'jobs'
|
|
parent_key = 'system_job_template'
|
|
|
|
|
|
class SystemJobTemplateNotificationTemplatesAnyList(SubListCreateAttachDetachAPIView):
|
|
|
|
model = models.NotificationTemplate
|
|
serializer_class = serializers.NotificationTemplateSerializer
|
|
parent_model = models.SystemJobTemplate
|
|
|
|
|
|
class SystemJobTemplateNotificationTemplatesStartedList(SystemJobTemplateNotificationTemplatesAnyList):
|
|
|
|
relationship = 'notification_templates_started'
|
|
|
|
|
|
class SystemJobTemplateNotificationTemplatesErrorList(SystemJobTemplateNotificationTemplatesAnyList):
|
|
|
|
relationship = 'notification_templates_error'
|
|
|
|
|
|
class SystemJobTemplateNotificationTemplatesSuccessList(SystemJobTemplateNotificationTemplatesAnyList):
|
|
|
|
relationship = 'notification_templates_success'
|
|
|
|
|
|
class JobList(ListAPIView):
|
|
|
|
model = models.Job
|
|
serializer_class = serializers.JobListSerializer
|
|
|
|
|
|
class JobDetail(UnifiedJobDeletionMixin, RetrieveDestroyAPIView):
|
|
|
|
model = models.Job
|
|
serializer_class = serializers.JobDetailSerializer
|
|
|
|
def update(self, request, *args, **kwargs):
|
|
obj = self.get_object()
|
|
# Only allow changes (PUT/PATCH) when job status is "new".
|
|
if obj.status != 'new':
|
|
return self.http_method_not_allowed(request, *args, **kwargs)
|
|
return super(JobDetail, self).update(request, *args, **kwargs)
|
|
|
|
|
|
class JobCredentialsList(SubListAPIView):
|
|
|
|
model = models.Credential
|
|
serializer_class = serializers.CredentialSerializer
|
|
parent_model = models.Job
|
|
relationship = 'credentials'
|
|
|
|
|
|
class JobLabelList(SubListAPIView):
|
|
|
|
model = models.Label
|
|
serializer_class = serializers.LabelSerializer
|
|
parent_model = models.Job
|
|
relationship = 'labels'
|
|
parent_key = 'job'
|
|
|
|
|
|
class WorkflowJobLabelList(JobLabelList):
|
|
parent_model = models.WorkflowJob
|
|
|
|
|
|
class JobActivityStreamList(SubListAPIView):
|
|
|
|
model = models.ActivityStream
|
|
serializer_class = serializers.ActivityStreamSerializer
|
|
parent_model = models.Job
|
|
relationship = 'activitystream_set'
|
|
search_fields = ('changes',)
|
|
|
|
|
|
class JobCancel(RetrieveAPIView):
|
|
|
|
model = models.Job
|
|
obj_permission_type = 'cancel'
|
|
serializer_class = serializers.JobCancelSerializer
|
|
|
|
def post(self, request, *args, **kwargs):
|
|
obj = self.get_object()
|
|
if obj.can_cancel:
|
|
obj.cancel()
|
|
return Response(status=status.HTTP_202_ACCEPTED)
|
|
else:
|
|
return self.http_method_not_allowed(request, *args, **kwargs)
|
|
|
|
|
|
class JobRelaunch(RetrieveAPIView):
|
|
|
|
model = models.Job
|
|
obj_permission_type = 'start'
|
|
serializer_class = serializers.JobRelaunchSerializer
|
|
|
|
def update_raw_data(self, data):
|
|
data = super(JobRelaunch, self).update_raw_data(data)
|
|
try:
|
|
obj = self.get_object()
|
|
except PermissionDenied:
|
|
return data
|
|
if obj:
|
|
needed_passwords = obj.passwords_needed_to_start
|
|
if needed_passwords:
|
|
data['credential_passwords'] = {}
|
|
for p in needed_passwords:
|
|
data['credential_passwords'][p] = u''
|
|
else:
|
|
data.pop('credential_passwords', None)
|
|
return data
|
|
|
|
@transaction.non_atomic_requests
|
|
def dispatch(self, *args, **kwargs):
|
|
return super(JobRelaunch, self).dispatch(*args, **kwargs)
|
|
|
|
def check_object_permissions(self, request, obj):
|
|
if request.method == 'POST' and obj:
|
|
relaunch_perm, messages = request.user.can_access_with_errors(self.model, 'start', obj)
|
|
if not relaunch_perm and 'detail' in messages:
|
|
self.permission_denied(request, message=messages['detail'])
|
|
return super(JobRelaunch, self).check_object_permissions(request, obj)
|
|
|
|
def post(self, request, *args, **kwargs):
|
|
obj = self.get_object()
|
|
context = self.get_serializer_context()
|
|
|
|
modified_data = request.data.copy()
|
|
modified_data.setdefault('credential_passwords', {})
|
|
for password in obj.passwords_needed_to_start:
|
|
if password in modified_data:
|
|
modified_data['credential_passwords'][password] = modified_data[password]
|
|
|
|
# Note: is_valid() may modify request.data
|
|
# It will remove any key/value pair who's key is not in the 'passwords_needed_to_start' list
|
|
serializer = self.serializer_class(data=modified_data, context=context, instance=obj)
|
|
if not serializer.is_valid():
|
|
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
|
|
|
copy_kwargs = {}
|
|
retry_hosts = serializer.validated_data.get('hosts', None)
|
|
if retry_hosts and retry_hosts != 'all':
|
|
if obj.status in ACTIVE_STATES:
|
|
return Response({'hosts': _(
|
|
'Wait until job finishes before retrying on {status_value} hosts.'
|
|
).format(status_value=retry_hosts)}, status=status.HTTP_400_BAD_REQUEST)
|
|
host_qs = obj.retry_qs(retry_hosts)
|
|
if not obj.job_events.filter(event='playbook_on_stats').exists():
|
|
return Response({'hosts': _(
|
|
'Cannot retry on {status_value} hosts, playbook stats not available.'
|
|
).format(status_value=retry_hosts)}, status=status.HTTP_400_BAD_REQUEST)
|
|
retry_host_list = host_qs.values_list('name', flat=True)
|
|
if len(retry_host_list) == 0:
|
|
return Response({'hosts': _(
|
|
'Cannot relaunch because previous job had 0 {status_value} hosts.'
|
|
).format(status_value=retry_hosts)}, status=status.HTTP_400_BAD_REQUEST)
|
|
copy_kwargs['limit'] = ','.join(retry_host_list)
|
|
|
|
new_job = obj.copy_unified_job(**copy_kwargs)
|
|
result = new_job.signal_start(**serializer.validated_data['credential_passwords'])
|
|
if not result:
|
|
data = dict(msg=_('Error starting job!'))
|
|
new_job.delete()
|
|
return Response(data, status=status.HTTP_400_BAD_REQUEST)
|
|
else:
|
|
data = serializers.JobSerializer(new_job, context=context).data
|
|
# Add job key to match what old relaunch returned.
|
|
data['job'] = new_job.id
|
|
headers = {'Location': new_job.get_absolute_url(request=request)}
|
|
return Response(data, status=status.HTTP_201_CREATED, headers=headers)
|
|
|
|
|
|
class JobCreateSchedule(RetrieveAPIView):
|
|
|
|
model = models.Job
|
|
obj_permission_type = 'start'
|
|
serializer_class = serializers.JobCreateScheduleSerializer
|
|
|
|
def post(self, request, *args, **kwargs):
|
|
obj = self.get_object()
|
|
|
|
if not obj.can_schedule:
|
|
if getattr(obj, 'passwords_needed_to_start', None):
|
|
return Response({"error": _('Cannot create schedule because job requires credential passwords.')},
|
|
status=status.HTTP_400_BAD_REQUEST)
|
|
try:
|
|
obj.launch_config
|
|
except ObjectDoesNotExist:
|
|
return Response({"error": _('Cannot create schedule because job was launched by legacy method.')},
|
|
status=status.HTTP_400_BAD_REQUEST)
|
|
return Response({"error": _('Cannot create schedule because a related resource is missing.')},
|
|
status=status.HTTP_400_BAD_REQUEST)
|
|
|
|
config = obj.launch_config
|
|
|
|
# Make up a name for the schedule, guarentee that it is unique
|
|
name = 'Auto-generated schedule from job {}'.format(obj.id)
|
|
existing_names = models.Schedule.objects.filter(name__startswith=name).values_list('name', flat=True)
|
|
if name in existing_names:
|
|
idx = 1
|
|
alt_name = '{} - number {}'.format(name, idx)
|
|
while alt_name in existing_names:
|
|
idx += 1
|
|
alt_name = '{} - number {}'.format(name, idx)
|
|
name = alt_name
|
|
|
|
schedule_data = dict(
|
|
name=name,
|
|
unified_job_template=obj.unified_job_template,
|
|
enabled=False,
|
|
rrule='{}Z RRULE:FREQ=MONTHLY;INTERVAL=1'.format(now().strftime('DTSTART:%Y%m%dT%H%M%S')),
|
|
extra_data=config.extra_data,
|
|
survey_passwords=config.survey_passwords,
|
|
inventory=config.inventory,
|
|
char_prompts=config.char_prompts,
|
|
credentials=set(config.credentials.all())
|
|
)
|
|
if not request.user.can_access(models.Schedule, 'add', schedule_data):
|
|
raise PermissionDenied()
|
|
|
|
creds_list = schedule_data.pop('credentials')
|
|
schedule = models.Schedule.objects.create(**schedule_data)
|
|
schedule.credentials.add(*creds_list)
|
|
|
|
data = serializers.ScheduleSerializer(schedule, context=self.get_serializer_context()).data
|
|
data.serializer.instance = None # hack to avoid permissions.py assuming this is Job model
|
|
headers = {'Location': schedule.get_absolute_url(request=request)}
|
|
return Response(data, status=status.HTTP_201_CREATED, headers=headers)
|
|
|
|
|
|
class JobNotificationsList(SubListAPIView):
|
|
|
|
model = models.Notification
|
|
serializer_class = serializers.NotificationSerializer
|
|
parent_model = models.Job
|
|
relationship = 'notifications'
|
|
search_fields = ('subject', 'notification_type', 'body',)
|
|
|
|
|
|
class BaseJobHostSummariesList(SubListAPIView):
|
|
|
|
model = models.JobHostSummary
|
|
serializer_class = serializers.JobHostSummarySerializer
|
|
parent_model = None # Subclasses must define this attribute.
|
|
relationship = 'job_host_summaries'
|
|
name = _('Job Host Summaries List')
|
|
search_fields = ('host_name',)
|
|
|
|
def get_queryset(self):
|
|
parent = self.get_parent_object()
|
|
self.check_parent_access(parent)
|
|
return getattr(parent, self.relationship).select_related('job', 'job__job_template', 'host')
|
|
|
|
|
|
class HostJobHostSummariesList(BaseJobHostSummariesList):
|
|
|
|
parent_model = models.Host
|
|
|
|
|
|
class GroupJobHostSummariesList(BaseJobHostSummariesList):
|
|
|
|
parent_model = models.Group
|
|
|
|
|
|
class JobJobHostSummariesList(BaseJobHostSummariesList):
|
|
|
|
parent_model = models.Job
|
|
|
|
|
|
class JobHostSummaryDetail(RetrieveAPIView):
|
|
|
|
model = models.JobHostSummary
|
|
serializer_class = serializers.JobHostSummarySerializer
|
|
|
|
|
|
class JobEventList(NoTruncateMixin, ListAPIView):
|
|
|
|
model = models.JobEvent
|
|
serializer_class = serializers.JobEventSerializer
|
|
search_fields = ('stdout',)
|
|
|
|
|
|
class JobEventDetail(RetrieveAPIView):
|
|
|
|
model = models.JobEvent
|
|
serializer_class = serializers.JobEventSerializer
|
|
|
|
def get_serializer_context(self):
|
|
context = super().get_serializer_context()
|
|
context.update(no_truncate=True)
|
|
return context
|
|
|
|
|
|
class JobEventChildrenList(NoTruncateMixin, SubListAPIView):
|
|
|
|
model = models.JobEvent
|
|
serializer_class = serializers.JobEventSerializer
|
|
parent_model = models.JobEvent
|
|
relationship = 'children'
|
|
name = _('Job Event Children List')
|
|
search_fields = ('stdout',)
|
|
|
|
def get_queryset(self):
|
|
parent_event = self.get_parent_object()
|
|
self.check_parent_access(parent_event)
|
|
qs = self.request.user.get_queryset(self.model).filter(parent_uuid=parent_event.uuid)
|
|
return qs
|
|
|
|
|
|
class JobEventHostsList(HostRelatedSearchMixin, SubListAPIView):
|
|
|
|
model = models.Host
|
|
serializer_class = serializers.HostSerializer
|
|
parent_model = models.JobEvent
|
|
relationship = 'hosts'
|
|
name = _('Job Event Hosts List')
|
|
|
|
def get_queryset(self):
|
|
parent_event = self.get_parent_object()
|
|
self.check_parent_access(parent_event)
|
|
qs = self.request.user.get_queryset(self.model).filter(job_events_as_primary_host=parent_event)
|
|
return qs
|
|
|
|
|
|
class BaseJobEventsList(NoTruncateMixin, SubListAPIView):
|
|
|
|
model = models.JobEvent
|
|
serializer_class = serializers.JobEventSerializer
|
|
parent_model = None # Subclasses must define this attribute.
|
|
relationship = 'job_events'
|
|
name = _('Job Events List')
|
|
search_fields = ('stdout',)
|
|
|
|
def finalize_response(self, request, response, *args, **kwargs):
|
|
response['X-UI-Max-Events'] = settings.MAX_UI_JOB_EVENTS
|
|
return super(BaseJobEventsList, self).finalize_response(request, response, *args, **kwargs)
|
|
|
|
|
|
class HostJobEventsList(BaseJobEventsList):
|
|
|
|
parent_model = models.Host
|
|
|
|
def get_queryset(self):
|
|
parent_obj = self.get_parent_object()
|
|
self.check_parent_access(parent_obj)
|
|
qs = self.request.user.get_queryset(self.model).filter(host=parent_obj)
|
|
return qs
|
|
|
|
|
|
class GroupJobEventsList(BaseJobEventsList):
|
|
|
|
parent_model = models.Group
|
|
|
|
|
|
class JobJobEventsList(BaseJobEventsList):
|
|
|
|
parent_model = models.Job
|
|
|
|
def get_queryset(self):
|
|
job = self.get_parent_object()
|
|
self.check_parent_access(job)
|
|
qs = job.job_events.select_related('host').order_by('start_line')
|
|
return qs.all()
|
|
|
|
|
|
class AdHocCommandList(ListCreateAPIView):
|
|
|
|
model = models.AdHocCommand
|
|
serializer_class = serializers.AdHocCommandListSerializer
|
|
always_allow_superuser = False
|
|
|
|
@transaction.non_atomic_requests
|
|
def dispatch(self, *args, **kwargs):
|
|
return super(AdHocCommandList, self).dispatch(*args, **kwargs)
|
|
|
|
def update_raw_data(self, data):
|
|
# Hide inventory and limit fields from raw data, since they will be set
|
|
# automatically by sub list create view.
|
|
parent_model = getattr(self, 'parent_model', None)
|
|
if parent_model in (models.Host, models.Group):
|
|
data.pop('inventory', None)
|
|
data.pop('limit', None)
|
|
return super(AdHocCommandList, self).update_raw_data(data)
|
|
|
|
def create(self, request, *args, **kwargs):
|
|
# Inject inventory ID and limit if parent objects is a host/group.
|
|
if hasattr(self, 'get_parent_object') and not getattr(self, 'parent_key', None):
|
|
data = request.data
|
|
# HACK: Make request data mutable.
|
|
if getattr(data, '_mutable', None) is False:
|
|
data._mutable = True
|
|
parent_obj = self.get_parent_object()
|
|
if isinstance(parent_obj, (models.Host, models.Group)):
|
|
data['inventory'] = parent_obj.inventory_id
|
|
data['limit'] = parent_obj.name
|
|
|
|
# Check for passwords needed before creating ad hoc command.
|
|
credential_pk = get_pk_from_dict(request.data, 'credential')
|
|
if credential_pk:
|
|
credential = get_object_or_400(models.Credential, pk=credential_pk)
|
|
needed = credential.passwords_needed
|
|
provided = dict([(field, request.data.get(field, '')) for field in needed])
|
|
if not all(provided.values()):
|
|
data = dict(passwords_needed_to_start=needed)
|
|
return Response(data, status=status.HTTP_400_BAD_REQUEST)
|
|
|
|
response = super(AdHocCommandList, self).create(request, *args, **kwargs)
|
|
if response.status_code != status.HTTP_201_CREATED:
|
|
return response
|
|
|
|
# Start ad hoc command running when created.
|
|
ad_hoc_command = get_object_or_400(self.model, pk=response.data['id'])
|
|
result = ad_hoc_command.signal_start(**request.data)
|
|
if not result:
|
|
data = dict(passwords_needed_to_start=ad_hoc_command.passwords_needed_to_start)
|
|
ad_hoc_command.delete()
|
|
return Response(data, status=status.HTTP_400_BAD_REQUEST)
|
|
return response
|
|
|
|
|
|
class InventoryAdHocCommandsList(AdHocCommandList, SubListCreateAPIView):
|
|
|
|
parent_model = models.Inventory
|
|
relationship = 'ad_hoc_commands'
|
|
parent_key = 'inventory'
|
|
|
|
|
|
class GroupAdHocCommandsList(AdHocCommandList, SubListCreateAPIView):
|
|
|
|
parent_model = models.Group
|
|
relationship = 'ad_hoc_commands'
|
|
|
|
|
|
class HostAdHocCommandsList(AdHocCommandList, SubListCreateAPIView):
|
|
|
|
parent_model = models.Host
|
|
relationship = 'ad_hoc_commands'
|
|
|
|
|
|
class AdHocCommandDetail(UnifiedJobDeletionMixin, RetrieveDestroyAPIView):
|
|
|
|
model = models.AdHocCommand
|
|
serializer_class = serializers.AdHocCommandDetailSerializer
|
|
|
|
|
|
class AdHocCommandCancel(RetrieveAPIView):
|
|
|
|
model = models.AdHocCommand
|
|
obj_permission_type = 'cancel'
|
|
serializer_class = serializers.AdHocCommandCancelSerializer
|
|
|
|
def post(self, request, *args, **kwargs):
|
|
obj = self.get_object()
|
|
if obj.can_cancel:
|
|
obj.cancel()
|
|
return Response(status=status.HTTP_202_ACCEPTED)
|
|
else:
|
|
return self.http_method_not_allowed(request, *args, **kwargs)
|
|
|
|
|
|
class AdHocCommandRelaunch(GenericAPIView):
|
|
|
|
model = models.AdHocCommand
|
|
obj_permission_type = 'start'
|
|
serializer_class = serializers.AdHocCommandRelaunchSerializer
|
|
|
|
# FIXME: Figure out why OPTIONS request still shows all fields.
|
|
|
|
@transaction.non_atomic_requests
|
|
def dispatch(self, *args, **kwargs):
|
|
return super(AdHocCommandRelaunch, self).dispatch(*args, **kwargs)
|
|
|
|
def get(self, request, *args, **kwargs):
|
|
obj = self.get_object()
|
|
data = dict(passwords_needed_to_start=obj.passwords_needed_to_start)
|
|
return Response(data)
|
|
|
|
def post(self, request, *args, **kwargs):
|
|
obj = self.get_object()
|
|
|
|
# Re-validate ad hoc command against serializer to check if module is
|
|
# still allowed.
|
|
data = {}
|
|
for field in ('job_type', 'inventory_id', 'limit', 'credential_id',
|
|
'module_name', 'module_args', 'forks', 'verbosity',
|
|
'extra_vars', 'become_enabled'):
|
|
if field.endswith('_id'):
|
|
data[field[:-3]] = getattr(obj, field)
|
|
else:
|
|
data[field] = getattr(obj, field)
|
|
serializer = serializers.AdHocCommandSerializer(data=data, context=self.get_serializer_context())
|
|
if not serializer.is_valid():
|
|
return Response(serializer.errors,
|
|
status=status.HTTP_400_BAD_REQUEST)
|
|
|
|
# Check for passwords needed before copying ad hoc command.
|
|
needed = obj.passwords_needed_to_start
|
|
provided = dict([(field, request.data.get(field, '')) for field in needed])
|
|
if not all(provided.values()):
|
|
data = dict(passwords_needed_to_start=needed)
|
|
return Response(data, status=status.HTTP_400_BAD_REQUEST)
|
|
|
|
# Copy and start the new ad hoc command.
|
|
new_ad_hoc_command = obj.copy()
|
|
result = new_ad_hoc_command.signal_start(**request.data)
|
|
if not result:
|
|
data = dict(passwords_needed_to_start=new_ad_hoc_command.passwords_needed_to_start)
|
|
new_ad_hoc_command.delete()
|
|
return Response(data, status=status.HTTP_400_BAD_REQUEST)
|
|
else:
|
|
data = serializers.AdHocCommandSerializer(new_ad_hoc_command, context=self.get_serializer_context()).data
|
|
# Add ad_hoc_command key to match what was previously returned.
|
|
data['ad_hoc_command'] = new_ad_hoc_command.id
|
|
headers = {'Location': new_ad_hoc_command.get_absolute_url(request=request)}
|
|
return Response(data, status=status.HTTP_201_CREATED, headers=headers)
|
|
|
|
|
|
class AdHocCommandEventList(NoTruncateMixin, ListAPIView):
|
|
|
|
model = models.AdHocCommandEvent
|
|
serializer_class = serializers.AdHocCommandEventSerializer
|
|
search_fields = ('stdout',)
|
|
|
|
|
|
class AdHocCommandEventDetail(RetrieveAPIView):
|
|
|
|
model = models.AdHocCommandEvent
|
|
serializer_class = serializers.AdHocCommandEventSerializer
|
|
|
|
def get_serializer_context(self):
|
|
context = super().get_serializer_context()
|
|
context.update(no_truncate=True)
|
|
return context
|
|
|
|
|
|
class BaseAdHocCommandEventsList(NoTruncateMixin, SubListAPIView):
|
|
|
|
model = models.AdHocCommandEvent
|
|
serializer_class = serializers.AdHocCommandEventSerializer
|
|
parent_model = None # Subclasses must define this attribute.
|
|
relationship = 'ad_hoc_command_events'
|
|
name = _('Ad Hoc Command Events List')
|
|
search_fields = ('stdout',)
|
|
|
|
|
|
class HostAdHocCommandEventsList(BaseAdHocCommandEventsList):
|
|
|
|
parent_model = models.Host
|
|
|
|
|
|
#class GroupJobEventsList(BaseJobEventsList):
|
|
# parent_model = Group
|
|
|
|
|
|
class AdHocCommandAdHocCommandEventsList(BaseAdHocCommandEventsList):
|
|
|
|
parent_model = models.AdHocCommand
|
|
|
|
|
|
class AdHocCommandActivityStreamList(SubListAPIView):
|
|
|
|
model = models.ActivityStream
|
|
serializer_class = serializers.ActivityStreamSerializer
|
|
parent_model = models.AdHocCommand
|
|
relationship = 'activitystream_set'
|
|
search_fields = ('changes',)
|
|
|
|
|
|
class AdHocCommandNotificationsList(SubListAPIView):
|
|
|
|
model = models.Notification
|
|
serializer_class = serializers.NotificationSerializer
|
|
parent_model = models.AdHocCommand
|
|
relationship = 'notifications'
|
|
search_fields = ('subject', 'notification_type', 'body',)
|
|
|
|
|
|
class SystemJobList(ListAPIView):
|
|
|
|
model = models.SystemJob
|
|
serializer_class = serializers.SystemJobListSerializer
|
|
|
|
def get(self, request, *args, **kwargs):
|
|
if not request.user.is_superuser and not request.user.is_system_auditor:
|
|
raise PermissionDenied(_("Superuser privileges needed."))
|
|
return super(SystemJobList, self).get(request, *args, **kwargs)
|
|
|
|
|
|
class SystemJobDetail(UnifiedJobDeletionMixin, RetrieveDestroyAPIView):
|
|
|
|
model = models.SystemJob
|
|
serializer_class = serializers.SystemJobSerializer
|
|
|
|
|
|
class SystemJobCancel(RetrieveAPIView):
|
|
|
|
model = models.SystemJob
|
|
obj_permission_type = 'cancel'
|
|
serializer_class = serializers.SystemJobCancelSerializer
|
|
|
|
def post(self, request, *args, **kwargs):
|
|
obj = self.get_object()
|
|
if obj.can_cancel:
|
|
obj.cancel()
|
|
return Response(status=status.HTTP_202_ACCEPTED)
|
|
else:
|
|
return self.http_method_not_allowed(request, *args, **kwargs)
|
|
|
|
|
|
class SystemJobNotificationsList(SubListAPIView):
|
|
|
|
model = models.Notification
|
|
serializer_class = serializers.NotificationSerializer
|
|
parent_model = models.SystemJob
|
|
relationship = 'notifications'
|
|
search_fields = ('subject', 'notification_type', 'body',)
|
|
|
|
|
|
class UnifiedJobTemplateList(ListAPIView):
|
|
|
|
model = models.UnifiedJobTemplate
|
|
serializer_class = serializers.UnifiedJobTemplateSerializer
|
|
search_fields = ('description', 'name', 'jobtemplate__playbook',)
|
|
|
|
|
|
class UnifiedJobList(ListAPIView):
|
|
|
|
model = models.UnifiedJob
|
|
serializer_class = serializers.UnifiedJobListSerializer
|
|
search_fields = ('description', 'name', 'job__playbook',)
|
|
|
|
|
|
def redact_ansi(line):
|
|
# Remove ANSI escape sequences used to embed event data.
|
|
line = re.sub(r'\x1b\[K(?:[A-Za-z0-9+/=]+\x1b\[\d+D)+\x1b\[K', '', line)
|
|
# Remove ANSI color escape sequences.
|
|
return re.sub(r'\x1b[^m]*m', '', line)
|
|
|
|
|
|
class StdoutFilter(object):
|
|
|
|
def __init__(self, fileobj):
|
|
self._functions = []
|
|
self.fileobj = fileobj
|
|
self.extra_data = ''
|
|
if hasattr(fileobj, 'close'):
|
|
self.close = fileobj.close
|
|
|
|
def read(self, size=-1):
|
|
data = self.extra_data
|
|
while size > 0 and len(data) < size:
|
|
line = self.fileobj.readline(size)
|
|
if not line:
|
|
break
|
|
line = self.process_line(line)
|
|
data += line
|
|
if size > 0 and len(data) > size:
|
|
self.extra_data = data[size:]
|
|
data = data[:size]
|
|
else:
|
|
self.extra_data = ''
|
|
return data
|
|
|
|
def register(self, func):
|
|
self._functions.append(func)
|
|
|
|
def process_line(self, line):
|
|
for func in self._functions:
|
|
line = func(line)
|
|
return line
|
|
|
|
|
|
class UnifiedJobStdout(RetrieveAPIView):
|
|
|
|
authentication_classes = api_settings.DEFAULT_AUTHENTICATION_CLASSES
|
|
serializer_class = serializers.UnifiedJobStdoutSerializer
|
|
renderer_classes = [renderers.BrowsableAPIRenderer, StaticHTMLRenderer,
|
|
renderers.PlainTextRenderer, renderers.AnsiTextRenderer,
|
|
JSONRenderer, renderers.DownloadTextRenderer,
|
|
renderers.AnsiDownloadRenderer]
|
|
filter_backends = ()
|
|
|
|
def retrieve(self, request, *args, **kwargs):
|
|
unified_job = self.get_object()
|
|
try:
|
|
target_format = request.accepted_renderer.format
|
|
if target_format in ('html', 'api', 'json'):
|
|
content_encoding = request.query_params.get('content_encoding', None)
|
|
start_line = request.query_params.get('start_line', 0)
|
|
end_line = request.query_params.get('end_line', None)
|
|
dark_val = request.query_params.get('dark', '')
|
|
dark = bool(dark_val and dark_val[0].lower() in ('1', 't', 'y'))
|
|
content_only = bool(target_format in ('api', 'json'))
|
|
dark_bg = (content_only and dark) or (not content_only and (dark or not dark_val))
|
|
content, start, end, absolute_end = unified_job.result_stdout_raw_limited(start_line, end_line)
|
|
|
|
# Remove any ANSI escape sequences containing job event data.
|
|
content = re.sub(r'\x1b\[K(?:[A-Za-z0-9+/=]+\x1b\[\d+D)+\x1b\[K', '', content)
|
|
|
|
body = ansiconv.to_html(html.escape(content))
|
|
|
|
context = {
|
|
'title': get_view_name(self.__class__),
|
|
'body': mark_safe(body),
|
|
'dark': dark_bg,
|
|
'content_only': content_only,
|
|
}
|
|
data = render_to_string('api/stdout.html', context).strip()
|
|
|
|
if target_format == 'api':
|
|
return Response(mark_safe(data))
|
|
if target_format == 'json':
|
|
content = content.encode('utf-8')
|
|
if content_encoding == 'base64':
|
|
content = b64encode(content)
|
|
return Response({'range': {'start': start, 'end': end, 'absolute_end': absolute_end}, 'content': content})
|
|
return Response(data)
|
|
elif target_format == 'txt':
|
|
return Response(unified_job.result_stdout)
|
|
elif target_format == 'ansi':
|
|
return Response(unified_job.result_stdout_raw)
|
|
elif target_format in {'txt_download', 'ansi_download'}:
|
|
filename = '{type}_{pk}{suffix}.txt'.format(
|
|
type=camelcase_to_underscore(unified_job.__class__.__name__),
|
|
pk=unified_job.id,
|
|
suffix='.ansi' if target_format == 'ansi_download' else ''
|
|
)
|
|
content_fd = unified_job.result_stdout_raw_handle(enforce_max_bytes=False)
|
|
redactor = StdoutFilter(content_fd)
|
|
if target_format == 'txt_download':
|
|
redactor.register(redact_ansi)
|
|
if type(unified_job) == models.ProjectUpdate:
|
|
redactor.register(UriCleaner.remove_sensitive)
|
|
response = HttpResponse(FileWrapper(redactor), content_type='text/plain')
|
|
response["Content-Disposition"] = 'attachment; filename="{}"'.format(filename)
|
|
return response
|
|
else:
|
|
return super(UnifiedJobStdout, self).retrieve(request, *args, **kwargs)
|
|
except models.StdoutMaxBytesExceeded as e:
|
|
response_message = _(
|
|
"Standard Output too large to display ({text_size} bytes), "
|
|
"only download supported for sizes over {supported_size} bytes.").format(
|
|
text_size=e.total, supported_size=e.supported
|
|
)
|
|
if request.accepted_renderer.format == 'json':
|
|
return Response({'range': {'start': 0, 'end': 1, 'absolute_end': 1}, 'content': response_message})
|
|
else:
|
|
return Response(response_message)
|
|
|
|
|
|
class ProjectUpdateStdout(UnifiedJobStdout):
|
|
|
|
model = models.ProjectUpdate
|
|
|
|
|
|
class InventoryUpdateStdout(UnifiedJobStdout):
|
|
|
|
model = models.InventoryUpdate
|
|
|
|
|
|
class JobStdout(UnifiedJobStdout):
|
|
|
|
model = models.Job
|
|
|
|
|
|
class AdHocCommandStdout(UnifiedJobStdout):
|
|
|
|
model = models.AdHocCommand
|
|
|
|
|
|
class NotificationTemplateList(ListCreateAPIView):
|
|
|
|
model = models.NotificationTemplate
|
|
serializer_class = serializers.NotificationTemplateSerializer
|
|
|
|
|
|
class NotificationTemplateDetail(RetrieveUpdateDestroyAPIView):
|
|
|
|
model = models.NotificationTemplate
|
|
serializer_class = serializers.NotificationTemplateSerializer
|
|
|
|
def delete(self, request, *args, **kwargs):
|
|
obj = self.get_object()
|
|
if not request.user.can_access(self.model, 'delete', obj):
|
|
return Response(status=status.HTTP_404_NOT_FOUND)
|
|
|
|
hours_old = now() - dateutil.relativedelta.relativedelta(hours=8)
|
|
if obj.notifications.filter(status='pending', created__gt=hours_old).exists():
|
|
return Response({"error": _("Delete not allowed while there are pending notifications")},
|
|
status=status.HTTP_405_METHOD_NOT_ALLOWED)
|
|
return super(NotificationTemplateDetail, self).delete(request, *args, **kwargs)
|
|
|
|
|
|
class NotificationTemplateTest(GenericAPIView):
|
|
'''Test a Notification Template'''
|
|
|
|
name = _('Notification Template Test')
|
|
model = models.NotificationTemplate
|
|
obj_permission_type = 'start'
|
|
serializer_class = serializers.EmptySerializer
|
|
|
|
def post(self, request, *args, **kwargs):
|
|
obj = self.get_object()
|
|
msg = "Tower Notification Test {} {}".format(obj.id, settings.TOWER_URL_BASE)
|
|
if obj.notification_type in ('email', 'pagerduty'):
|
|
body = "Ansible Tower Test Notification {} {}".format(obj.id, settings.TOWER_URL_BASE)
|
|
elif obj.notification_type in ('webhook', 'grafana'):
|
|
body = '{{"body": "Ansible Tower Test Notification {} {}"}}'.format(obj.id, settings.TOWER_URL_BASE)
|
|
else:
|
|
body = {"body": "Ansible Tower Test Notification {} {}".format(obj.id, settings.TOWER_URL_BASE)}
|
|
notification = obj.generate_notification(msg, body)
|
|
|
|
if not notification:
|
|
return Response({}, status=status.HTTP_400_BAD_REQUEST)
|
|
else:
|
|
connection.on_commit(lambda: send_notifications.delay([notification.id]))
|
|
data = OrderedDict()
|
|
data['notification'] = notification.id
|
|
data.update(serializers.NotificationSerializer(notification, context=self.get_serializer_context()).to_representation(notification))
|
|
headers = {'Location': notification.get_absolute_url(request=request)}
|
|
return Response(data,
|
|
headers=headers,
|
|
status=status.HTTP_202_ACCEPTED)
|
|
|
|
|
|
class NotificationTemplateNotificationList(SubListAPIView):
|
|
|
|
model = models.Notification
|
|
serializer_class = serializers.NotificationSerializer
|
|
parent_model = models.NotificationTemplate
|
|
relationship = 'notifications'
|
|
parent_key = 'notification_template'
|
|
search_fields = ('subject', 'notification_type', 'body',)
|
|
|
|
|
|
class NotificationTemplateCopy(CopyAPIView):
|
|
|
|
model = models.NotificationTemplate
|
|
copy_return_serializer_class = serializers.NotificationTemplateSerializer
|
|
|
|
|
|
class NotificationList(ListAPIView):
|
|
|
|
model = models.Notification
|
|
serializer_class = serializers.NotificationSerializer
|
|
search_fields = ('subject', 'notification_type', 'body',)
|
|
|
|
|
|
class NotificationDetail(RetrieveAPIView):
|
|
|
|
model = models.Notification
|
|
serializer_class = serializers.NotificationSerializer
|
|
|
|
|
|
class LabelList(ListCreateAPIView):
|
|
|
|
model = models.Label
|
|
serializer_class = serializers.LabelSerializer
|
|
|
|
|
|
class LabelDetail(RetrieveUpdateAPIView):
|
|
|
|
model = models.Label
|
|
serializer_class = serializers.LabelSerializer
|
|
|
|
|
|
class ActivityStreamList(SimpleListAPIView):
|
|
|
|
model = models.ActivityStream
|
|
serializer_class = serializers.ActivityStreamSerializer
|
|
search_fields = ('changes',)
|
|
|
|
|
|
class ActivityStreamDetail(RetrieveAPIView):
|
|
|
|
model = models.ActivityStream
|
|
serializer_class = serializers.ActivityStreamSerializer
|
|
|
|
|
|
class RoleList(ListAPIView):
|
|
|
|
model = models.Role
|
|
serializer_class = serializers.RoleSerializer
|
|
permission_classes = (IsAuthenticated,)
|
|
search_fields = ('role_field', 'content_type__model',)
|
|
|
|
|
|
class RoleDetail(RetrieveAPIView):
|
|
|
|
model = models.Role
|
|
serializer_class = serializers.RoleSerializer
|
|
|
|
|
|
class RoleUsersList(SubListAttachDetachAPIView):
|
|
|
|
model = models.User
|
|
serializer_class = serializers.UserSerializer
|
|
parent_model = models.Role
|
|
relationship = 'members'
|
|
ordering = ('username',)
|
|
|
|
def get_queryset(self):
|
|
role = self.get_parent_object()
|
|
self.check_parent_access(role)
|
|
return role.members.all()
|
|
|
|
def post(self, request, *args, **kwargs):
|
|
# Forbid implicit user creation here
|
|
sub_id = request.data.get('id', None)
|
|
if not sub_id:
|
|
return super(RoleUsersList, self).post(request)
|
|
|
|
user = get_object_or_400(models.User, pk=sub_id)
|
|
role = self.get_parent_object()
|
|
|
|
credential_content_type = ContentType.objects.get_for_model(models.Credential)
|
|
if role.content_type == credential_content_type:
|
|
if 'disassociate' not in request.data and role.content_object.organization and user not in role.content_object.organization.member_role:
|
|
data = dict(msg=_("You cannot grant credential access to a user not in the credentials' organization"))
|
|
return Response(data, status=status.HTTP_400_BAD_REQUEST)
|
|
|
|
if not role.content_object.organization and not request.user.is_superuser:
|
|
data = dict(msg=_("You cannot grant private credential access to another user"))
|
|
return Response(data, status=status.HTTP_400_BAD_REQUEST)
|
|
|
|
return super(RoleUsersList, self).post(request, *args, **kwargs)
|
|
|
|
|
|
class RoleTeamsList(SubListAttachDetachAPIView):
|
|
|
|
model = models.Team
|
|
serializer_class = serializers.TeamSerializer
|
|
parent_model = models.Role
|
|
relationship = 'member_role.parents'
|
|
permission_classes = (IsAuthenticated,)
|
|
|
|
def get_queryset(self):
|
|
role = self.get_parent_object()
|
|
self.check_parent_access(role)
|
|
return models.Team.objects.filter(member_role__children=role)
|
|
|
|
def post(self, request, pk, *args, **kwargs):
|
|
sub_id = request.data.get('id', None)
|
|
if not sub_id:
|
|
return super(RoleTeamsList, self).post(request)
|
|
|
|
team = get_object_or_400(models.Team, pk=sub_id)
|
|
role = models.Role.objects.get(pk=self.kwargs['pk'])
|
|
|
|
organization_content_type = ContentType.objects.get_for_model(models.Organization)
|
|
if role.content_type == organization_content_type and role.role_field in ['member_role', 'admin_role']:
|
|
data = dict(msg=_("You cannot assign an Organization participation role as a child role for a Team."))
|
|
return Response(data, status=status.HTTP_400_BAD_REQUEST)
|
|
|
|
credential_content_type = ContentType.objects.get_for_model(models.Credential)
|
|
if role.content_type == credential_content_type:
|
|
if not role.content_object.organization or role.content_object.organization.id != team.organization.id:
|
|
data = dict(msg=_("You cannot grant credential access to a team when the Organization field isn't set, or belongs to a different organization"))
|
|
return Response(data, status=status.HTTP_400_BAD_REQUEST)
|
|
|
|
action = 'attach'
|
|
if request.data.get('disassociate', None):
|
|
action = 'unattach'
|
|
|
|
if role.is_singleton() and action == 'attach':
|
|
data = dict(msg=_("You cannot grant system-level permissions to a team."))
|
|
return Response(data, status=status.HTTP_400_BAD_REQUEST)
|
|
|
|
if not request.user.can_access(self.parent_model, action, role, team,
|
|
self.relationship, request.data,
|
|
skip_sub_obj_read_check=False):
|
|
raise PermissionDenied()
|
|
if request.data.get('disassociate', None):
|
|
team.member_role.children.remove(role)
|
|
else:
|
|
team.member_role.children.add(role)
|
|
return Response(status=status.HTTP_204_NO_CONTENT)
|
|
|
|
|
|
class RoleParentsList(SubListAPIView):
|
|
|
|
model = models.Role
|
|
serializer_class = serializers.RoleSerializer
|
|
parent_model = models.Role
|
|
relationship = 'parents'
|
|
permission_classes = (IsAuthenticated,)
|
|
search_fields = ('role_field', 'content_type__model',)
|
|
|
|
def get_queryset(self):
|
|
role = models.Role.objects.get(pk=self.kwargs['pk'])
|
|
return models.Role.filter_visible_roles(self.request.user, role.parents.all())
|
|
|
|
|
|
class RoleChildrenList(SubListAPIView):
|
|
|
|
model = models.Role
|
|
serializer_class = serializers.RoleSerializer
|
|
parent_model = models.Role
|
|
relationship = 'children'
|
|
permission_classes = (IsAuthenticated,)
|
|
search_fields = ('role_field', 'content_type__model',)
|
|
|
|
def get_queryset(self):
|
|
role = models.Role.objects.get(pk=self.kwargs['pk'])
|
|
return models.Role.filter_visible_roles(self.request.user, role.children.all())
|
|
|
|
|
|
# Create view functions for all of the class-based views to simplify inclusion
|
|
# in URL patterns and reverse URL lookups, converting CamelCase names to
|
|
# lowercase_with_underscore (e.g. MyView.as_view() becomes my_view).
|
|
this_module = sys.modules[__name__]
|
|
for attr, value in list(locals().items()):
|
|
if isinstance(value, type) and issubclass(value, APIView):
|
|
name = camelcase_to_underscore(attr)
|
|
view = value.as_view()
|
|
setattr(this_module, name, view)
|
|
|
|
|
|
class WorkflowApprovalTemplateDetail(RelatedJobsPreventDeleteMixin, RetrieveUpdateDestroyAPIView):
|
|
|
|
model = models.WorkflowApprovalTemplate
|
|
serializer_class = serializers.WorkflowApprovalTemplateSerializer
|
|
|
|
|
|
class WorkflowApprovalTemplateJobsList(SubListAPIView):
|
|
|
|
model = models.WorkflowApproval
|
|
serializer_class = serializers.WorkflowApprovalListSerializer
|
|
parent_model = models.WorkflowApprovalTemplate
|
|
relationship = 'approvals'
|
|
parent_key = 'workflow_approval_template'
|
|
|
|
|
|
class WorkflowApprovalList(ListCreateAPIView):
|
|
|
|
model = models.WorkflowApproval
|
|
serializer_class = serializers.WorkflowApprovalListSerializer
|
|
|
|
def get(self, request, *args, **kwargs):
|
|
return super(WorkflowApprovalList, self).get(request, *args, **kwargs)
|
|
|
|
|
|
class WorkflowApprovalDetail(UnifiedJobDeletionMixin, RetrieveDestroyAPIView):
|
|
|
|
model = models.WorkflowApproval
|
|
serializer_class = serializers.WorkflowApprovalSerializer
|
|
|
|
|
|
class WorkflowApprovalApprove(RetrieveAPIView):
|
|
model = models.WorkflowApproval
|
|
serializer_class = serializers.WorkflowApprovalViewSerializer
|
|
permission_classes = (WorkflowApprovalPermission,)
|
|
|
|
def post(self, request, *args, **kwargs):
|
|
obj = self.get_object()
|
|
if not request.user.can_access(models.WorkflowApproval, 'approve_or_deny', obj):
|
|
raise PermissionDenied(detail=_("User does not have permission to approve or deny this workflow."))
|
|
if obj.status != 'pending':
|
|
return Response({"error": _("This workflow step has already been approved or denied.")}, status=status.HTTP_400_BAD_REQUEST)
|
|
obj.approve(request)
|
|
return Response(status=status.HTTP_204_NO_CONTENT)
|
|
|
|
|
|
class WorkflowApprovalDeny(RetrieveAPIView):
|
|
model = models.WorkflowApproval
|
|
serializer_class = serializers.WorkflowApprovalViewSerializer
|
|
permission_classes = (WorkflowApprovalPermission,)
|
|
|
|
def post(self, request, *args, **kwargs):
|
|
obj = self.get_object()
|
|
if not request.user.can_access(models.WorkflowApproval, 'approve_or_deny', obj):
|
|
raise PermissionDenied(detail=_("User does not have permission to approve or deny this workflow."))
|
|
if obj.status != 'pending':
|
|
return Response({"error": _("This workflow step has already been approved or denied.")}, status=status.HTTP_400_BAD_REQUEST)
|
|
obj.deny(request)
|
|
return Response(status=status.HTTP_204_NO_CONTENT)
|