# Copyright (c) 2015 Ansible, Inc. # All Rights Reserved. # Python import os import re import cgi import datetime import dateutil import time import socket import subprocess import sys import logging from base64 import b64encode from collections import OrderedDict # Django from django.conf import settings from django.contrib.auth.models import User, AnonymousUser from django.core.cache import cache from django.core.exceptions import FieldError from django.db.models import Q, Count, F from django.db import IntegrityError, transaction, connection from django.shortcuts import get_object_or_404 from django.utils.encoding import smart_text, force_text 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.core.servers.basehttp import FileWrapper 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 PermissionDenied, ParseError from rest_framework.parsers import FormParser from rest_framework.permissions import AllowAny, IsAuthenticated from rest_framework.response import Response from rest_framework.settings import api_settings from rest_framework.views import exception_handler 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.backends.utils import load_backends # AWX from awx.main.tasks import send_notifications from awx.main.access import get_user_queryset from awx.main.ha import is_ha_environment from awx.api.authentication import TaskAuthentication, TokenGetAuthentication from awx.api.generics import get_view_name from awx.api.generics import * # noqa from awx.api.versioning import reverse, get_request_version from awx.conf.license import get_license, feature_enabled, feature_exists, LicenseForbids from awx.main.models import * # noqa from awx.main.utils import * # noqa from awx.main.utils import ( callback_filter_out_ansible_extra_vars ) from awx.api.permissions import * # noqa from awx.api.renderers import * # noqa from awx.api.serializers import * # noqa from awx.api.metadata import RoleMetadata from awx.main.consumers import emit_channel_notification from awx.main.models.unified_jobs import ACTIVE_STATES from awx.main.scheduler.tasks import run_job_complete from awx.main.fields import DynamicFilterField 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]) return exception_handler(exc, context) class ActivityStreamEnforcementMixin(object): ''' Mixin to check that license supports activity streams. ''' def check_permissions(self, request): if not feature_enabled('activity_streams'): raise LicenseForbids(_('Your license does not allow use of the activity stream.')) return super(ActivityStreamEnforcementMixin, self).check_permissions(request) class SystemTrackingEnforcementMixin(object): ''' Mixin to check that license supports system tracking. ''' def check_permissions(self, request): if not feature_enabled('system_tracking'): raise LicenseForbids(_('Your license does not permit use of system tracking.')) return super(SystemTrackingEnforcementMixin, self).check_permissions(request) class WorkflowsEnforcementMixin(object): ''' Mixin to check that license supports workflows. ''' def check_permissions(self, request): if not feature_enabled('workflows'): raise LicenseForbids(_('Your license does not allow use of workflows.')) return super(WorkflowsEnforcementMixin, self).check_permissions(request) class ApiRootView(APIView): authentication_classes = [] permission_classes = (AllowAny,) view_name = _('REST API') versioning_class = None def get(self, request, format=None): ''' list supported API versions ''' v1 = reverse('api:api_v1_root_view', kwargs={'version': 'v1'}) v2 = reverse('api:api_v2_root_view', kwargs={'version': 'v2'}) data = dict( description = _('Ansible Tower REST API'), current_version = v2, available_versions = dict(v1 = v1, v2 = v2), ) if feature_enabled('rebranding'): data['custom_logo'] = settings.CUSTOM_LOGO data['custom_login_info'] = settings.CUSTOM_LOGIN_INFO return Response(data) class ApiVersionRootView(APIView): authentication_classes = [] permission_classes = (AllowAny,) def get(self, request, format=None): ''' list top level resources ''' data = OrderedDict() data['authtoken'] = reverse('api:auth_token_view', request=request) data['ping'] = reverse('api:api_v1_ping_view', request=request) data['config'] = reverse('api:api_v1_config_view', request=request) data['settings'] = reverse('api:setting_category_list', request=request) data['me'] = reverse('api:user_me_list', request=request) data['dashboard'] = reverse('api:dashboard_view', request=request) data['organizations'] = reverse('api:organization_list', request=request) data['users'] = reverse('api:user_list', request=request) data['projects'] = reverse('api:project_list', request=request) data['project_updates'] = reverse('api:project_update_list', request=request) data['teams'] = reverse('api:team_list', request=request) data['credentials'] = reverse('api:credential_list', request=request) if get_request_version(request) > 1: data['credential_types'] = reverse('api:credential_type_list', request=request) data['inventory'] = reverse('api:inventory_list', request=request) data['inventory_scripts'] = reverse('api:inventory_script_list', request=request) data['inventory_sources'] = reverse('api:inventory_source_list', request=request) data['inventory_updates'] = reverse('api:inventory_update_list', request=request) data['groups'] = reverse('api:group_list', request=request) data['hosts'] = reverse('api:host_list', request=request) data['job_templates'] = reverse('api:job_template_list', request=request) data['jobs'] = reverse('api:job_list', request=request) data['job_events'] = reverse('api:job_event_list', request=request) data['ad_hoc_commands'] = reverse('api:ad_hoc_command_list', request=request) data['system_job_templates'] = reverse('api:system_job_template_list', request=request) data['system_jobs'] = reverse('api:system_job_list', request=request) data['schedules'] = reverse('api:schedule_list', request=request) data['roles'] = reverse('api:role_list', request=request) data['notification_templates'] = reverse('api:notification_template_list', request=request) data['notifications'] = reverse('api:notification_list', request=request) data['labels'] = reverse('api:label_list', request=request) data['unified_job_templates'] = reverse('api:unified_job_template_list', request=request) data['unified_jobs'] = reverse('api:unified_job_list', request=request) data['activity_stream'] = reverse('api:activity_stream_list', request=request) data['workflow_job_templates'] = reverse('api:workflow_job_template_list', request=request) data['workflow_jobs'] = reverse('api:workflow_job_list', request=request) data['workflow_job_template_nodes'] = reverse('api:workflow_job_template_node_list', request=request) data['workflow_job_nodes'] = reverse('api:workflow_job_node_list', request=request) return Response(data) class ApiV1RootView(ApiVersionRootView): view_name = _('Version 1') class ApiV2RootView(ApiVersionRootView): view_name = _('Version 2') new_in_320 = True new_in_api_v2 = True class ApiV1PingView(APIView): """A simple view that reports very basic information about this Tower instance, which is acceptable to be public information. """ permission_classes = (AllowAny,) authentication_classes = () view_name = _('Ping') new_in_210 = True def get(self, request, format=None): """Return some basic information about this Tower instance. Everything returned here should be considered public / insecure, as this requires no auth and is intended for use by the installer process. """ active_tasks = cache.get("active_celery_tasks", None) response = { 'ha': is_ha_environment(), 'version': get_awx_version(), 'active_node': settings.CLUSTER_HOST_ID, } if not isinstance(request.user, AnonymousUser): response['celery_active_tasks'] = json.loads(active_tasks) if active_tasks is not None else None response['instances'] = [] for instance in Instance.objects.all(): response['instances'].append(dict(node=instance.hostname, heartbeat=instance.modified, capacity=instance.capacity)) response['instances'].sort() return Response(response) class ApiV1ConfigView(APIView): permission_classes = (IsAuthenticated,) view_name = _('Configuration') def check_permissions(self, request): super(ApiV1ConfigView, self).check_permissions(request) if not request.user.is_superuser and request.method.lower() not in {'options', 'head', 'get'}: self.permission_denied(request) # Raises PermissionDenied exception. def get(self, request, format=None): '''Return various sitewide configuration settings.''' if request.user.is_superuser or request.user.is_system_auditor: license_data = get_license(show_key=True) else: license_data = get_license(show_key=False) if not license_data.get('valid_key', False): license_data = {} if license_data and 'features' in license_data and 'activity_streams' in license_data['features']: # FIXME: Make the final setting value dependent on the feature? license_data['features']['activity_streams'] &= settings.ACTIVITY_STREAM_ENABLED pendo_state = settings.PENDO_TRACKING_STATE if settings.PENDO_TRACKING_STATE in ('off', 'anonymous', 'detailed') else 'off' data = dict( time_zone=settings.TIME_ZONE, license_info=license_data, version=get_awx_version(), ansible_version=get_ansible_version(), eula=render_to_string("eula.md"), analytics_status=pendo_state ) # If LDAP is enabled, user_ldap_fields will return a list of field # names that are managed by LDAP and should be read-only for users with # a non-empty ldap_dn attribute. if getattr(settings, 'AUTH_LDAP_SERVER_URI', None) and feature_enabled('ldap'): user_ldap_fields = ['username', 'password'] user_ldap_fields.extend(getattr(settings, 'AUTH_LDAP_USER_ATTR_MAP', {}).keys()) user_ldap_fields.extend(getattr(settings, 'AUTH_LDAP_USER_FLAGS_BY_GROUP', {}).keys()) data['user_ldap_fields'] = user_ldap_fields if request.user.is_superuser \ or request.user.is_system_auditor \ or Organization.accessible_objects(request.user, 'admin_role').exists() \ or Organization.accessible_objects(request.user, 'auditor_role').exists(): data.update(dict( project_base_dir = settings.PROJECTS_ROOT, project_local_paths = Project.get_local_path_choices(), )) return Response(data) def post(self, request): if not isinstance(request.data, dict): return Response({"error": _("Invalid license data")}, status=status.HTTP_400_BAD_REQUEST) if "eula_accepted" not in request.data: return Response({"error": _("Missing 'eula_accepted' property")}, status=status.HTTP_400_BAD_REQUEST) try: eula_accepted = to_python_boolean(request.data["eula_accepted"]) except ValueError: return Response({"error": _("'eula_accepted' value is invalid")}, status=status.HTTP_400_BAD_REQUEST) if not eula_accepted: return Response({"error": _("'eula_accepted' must be True")}, status=status.HTTP_400_BAD_REQUEST) request.data.pop("eula_accepted") try: data_actual = json.dumps(request.data) except Exception: logger.info(smart_text(u"Invalid JSON submitted for Tower license."), extra=dict(actor=request.user.username)) return Response({"error": _("Invalid JSON")}, status=status.HTTP_400_BAD_REQUEST) try: from awx.main.task_engine import TaskEnhancer license_data = json.loads(data_actual) license_data_validated = TaskEnhancer(**license_data).validate_enhancements() except Exception: logger.warning(smart_text(u"Invalid Tower license submitted."), extra=dict(actor=request.user.username)) return Response({"error": _("Invalid License")}, status=status.HTTP_400_BAD_REQUEST) # If the license is valid, write it to the database. if license_data_validated['valid_key']: settings.LICENSE = license_data settings.TOWER_URL_BASE = "{}://{}".format(request.scheme, request.get_host()) return Response(license_data_validated) logger.warning(smart_text(u"Invalid Tower license submitted."), extra=dict(actor=request.user.username)) return Response({"error": _("Invalid license")}, status=status.HTTP_400_BAD_REQUEST) def delete(self, request): try: settings.LICENSE = {} return Response(status=status.HTTP_204_NO_CONTENT) except: # FIX: Log return Response({"error": _("Failed to remove license (%s)") % has_error}, status=status.HTTP_400_BAD_REQUEST) class DashboardView(APIView): view_name = _("Dashboard") new_in_14 = True 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, Inventory) inventory_with_failed_hosts = user_inventory.filter(hosts_with_active_failures__gt=0) user_inventory_external = user_inventory.filter(has_inventory_sources=True) failed_inventory = sum(i.inventory_sources_with_failures for i in user_inventory) 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, InventorySource) rax_inventory_sources = user_inventory_sources.filter(source='rax') rax_inventory_failed = rax_inventory_sources.filter(status='failed') ec2_inventory_sources = user_inventory_sources.filter(source='ec2') ec2_inventory_failed = ec2_inventory_sources.filter(status='failed') data['inventory_sources'] = {} data['inventory_sources']['rax'] = {'url': reverse('api:inventory_source_list', request=request) + "?source=rax", 'label': 'Rackspace', 'failures_url': reverse('api:inventory_source_list', request=request) + "?source=rax&status=failed", 'total': rax_inventory_sources.count(), 'failed': rax_inventory_failed.count()} 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, Group) groups_job_failed = (Group.objects.filter(hosts_with_active_failures__gt=0) | Group.objects.filter(groups_with_active_failures__gt=0)).count() groups_inventory_failed = Group.objects.filter(inventory_sources__last_job_failed=True).count() data['groups'] = {'url': reverse('api:group_list', request=request), 'failures_url': reverse('api:group_list', request=request) + "?has_active_failures=True", 'total': user_groups.count(), 'job_failed': groups_job_failed, 'inventory_failed': groups_inventory_failed} user_hosts = get_user_queryset(request.user, Host) user_hosts_failed = user_hosts.filter(has_active_failures=True) data['hosts'] = {'url': reverse('api:host_list', request=request), 'failures_url': reverse('api:host_list', request=request) + "?has_active_failures=True", 'total': user_hosts.count(), 'failed': user_hosts_failed.count()} user_projects = get_user_queryset(request.user, 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) hg_projects = user_projects.filter(scm_type='hg') hg_failed_projects = hg_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']['hg'] = {'url': reverse('api:project_list', request=request) + "?scm_type=hg", 'label': 'Mercurial', 'failures_url': reverse('api:project_list', request=request) + "?scm_type=hg&last_job_failed=True", 'total': hg_projects.count(), 'failed': hg_failed_projects.count()} user_jobs = get_user_queryset(request.user, Job) user_failed_jobs = user_jobs.filter(failed=True) data['jobs'] = {'url': reverse('api:job_list', request=request), 'failure_url': reverse('api:job_list', request=request) + "?failed=True", 'total': user_jobs.count(), 'failed': user_failed_jobs.count()} user_list = get_user_queryset(request.user, User) team_list = get_user_queryset(request.user, Team) credential_list = get_user_queryset(request.user, Credential) job_template_list = get_user_queryset(request.user, JobTemplate) organization_list = get_user_queryset(request.user, 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): view_name = _("Dashboard Jobs Graphs") new_in_200 = True 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, UnifiedJob) 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=InventoryUpdate) failed_query = failed_query.filter(instance_of=InventoryUpdate) elif job_type == 'playbook_run': success_query = success_query.filter(instance_of=Job) failed_query = failed_query.filter(instance_of=Job) elif job_type == 'scm_update': success_query = success_query.filter(instance_of=ProjectUpdate) failed_query = failed_query.filter(instance_of=ProjectUpdate) success_qss = qsstats.QuerySetStats(success_query, 'finished') failed_qss = qsstats.QuerySetStats(failed_query, 'finished') start_date = datetime.datetime.utcnow() if period == 'month': end_date = start_date - dateutil.relativedelta.relativedelta(months=1) 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 ScheduleList(ListAPIView): view_name = _("Schedules") model = Schedule serializer_class = ScheduleSerializer new_in_148 = True class ScheduleDetail(RetrieveUpdateDestroyAPIView): model = Schedule serializer_class = ScheduleSerializer new_in_148 = True class ScheduleUnifiedJobsList(SubListAPIView): model = UnifiedJob serializer_class = UnifiedJobSerializer parent_model = Schedule relationship = 'unifiedjob_set' view_name = _('Schedule Jobs List') new_in_148 = True class AuthView(APIView): authentication_classes = [] permission_classes = (AllowAny,) new_in_240 = True 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 = 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: if (not feature_exists('enterprise_auth') and not feature_enabled('ldap')) or \ (not feature_enabled('enterprise_auth') and name in ['saml', 'radius']): continue 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 AuthTokenView(APIView): authentication_classes = [] permission_classes = (AllowAny,) serializer_class = AuthTokenSerializer model = AuthToken def get_serializer(self, *args, **kwargs): serializer = self.serializer_class(*args, **kwargs) # Override when called from browsable API to generate raw data form; # update serializer "validated" data to be displayed by the raw data # form. if hasattr(self, '_raw_data_form_marker'): # Always remove read only fields from serializer. for name, field in serializer.fields.items(): if getattr(field, 'read_only', None): del serializer.fields[name] serializer._data = self.update_raw_data(serializer.data) return serializer def post(self, request): serializer = self.get_serializer(data=request.data) if serializer.is_valid(): request_hash = AuthToken.get_request_hash(self.request) try: token = AuthToken.objects.filter(user=serializer.validated_data['user'], request_hash=request_hash, expires__gt=now(), reason='')[0] token.refresh() if 'username' in request.data: logger.info(smart_text(u"User {} logged in".format(request.data['username'])), extra=dict(actor=request.data['username'])) except IndexError: token = AuthToken.objects.create(user=serializer.validated_data['user'], request_hash=request_hash) if 'username' in request.data: logger.info(smart_text(u"User {} logged in".format(request.data['username'])), extra=dict(actor=request.data['username'])) # Get user un-expired tokens that are not invalidated that are # over the configured limit. # Mark them as invalid and inform the user invalid_tokens = AuthToken.get_tokens_over_limit(serializer.validated_data['user']) for t in invalid_tokens: emit_channel_notification('control-limit_reached', dict(group_name='control', reason=force_text(AuthToken.reason_long('limit_reached')), token_key=t.key)) t.invalidate(reason='limit_reached') # Note: This header is normally added in the middleware whenever an # auth token is included in the request header. headers = { 'Auth-Token-Timeout': int(settings.AUTH_TOKEN_EXPIRATION) } return Response({'token': token.key, 'expires': token.expires}, headers=headers) if 'username' in request.data: logger.warning(smart_text(u"Login failed for user {}".format(request.data['username'])), extra=dict(actor=request.data['username'])) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) def delete(self, request): if 'HTTP_AUTHORIZATION' in request.META: token_match = re.match("Token\s(.+)", request.META['HTTP_AUTHORIZATION']) if token_match: filter_tokens = AuthToken.objects.filter(key=token_match.groups()[0]) if filter_tokens.exists(): filter_tokens[0].invalidate() return Response(status=status.HTTP_204_NO_CONTENT) class OrganizationCountsMixin(object): def get_serializer_context(self, *args, **kwargs): full_context = super(OrganizationCountsMixin, self).get_serializer_context(*args, **kwargs) if self.request is None: return full_context db_results = {} org_qs = self.model.accessible_objects(self.request.user, 'read_role') org_id_list = org_qs.values('id') if len(org_id_list) == 0: if self.request.method == 'POST': full_context['related_field_counts'] = {} return full_context inv_qs = Inventory.accessible_objects(self.request.user, 'read_role') project_qs = Project.accessible_objects(self.request.user, 'read_role') # Produce counts of Foreign Key relationships db_results['inventories'] = inv_qs\ .values('organization').annotate(Count('organization')).order_by('organization') db_results['teams'] = Team.accessible_objects( self.request.user, 'read_role').values('organization').annotate( Count('organization')).order_by('organization') JT_project_reference = 'project__organization' JT_inventory_reference = 'inventory__organization' db_results['job_templates_project'] = JobTemplate.accessible_objects( self.request.user, 'read_role').exclude( project__organization=F(JT_inventory_reference)).values(JT_project_reference).annotate( Count(JT_project_reference)).order_by(JT_project_reference) db_results['job_templates_inventory'] = JobTemplate.accessible_objects( self.request.user, 'read_role').values(JT_inventory_reference).annotate( Count(JT_inventory_reference)).order_by(JT_inventory_reference) db_results['projects'] = project_qs\ .values('organization').annotate(Count('organization')).order_by('organization') # Other members and admins of organization are always viewable db_results['users'] = org_qs.annotate( users=Count('member_role__members', distinct=True), admins=Count('admin_role__members', distinct=True) ).values('id', 'users', 'admins') count_context = {} for org in org_id_list: org_id = org['id'] count_context[org_id] = { 'inventories': 0, 'teams': 0, 'users': 0, 'job_templates': 0, 'admins': 0, 'projects': 0} for res, count_qs in db_results.items(): if res == 'job_templates_project': org_reference = JT_project_reference elif res == 'job_templates_inventory': org_reference = JT_inventory_reference elif res == 'users': org_reference = 'id' else: org_reference = 'organization' for entry in count_qs: org_id = entry[org_reference] if org_id in count_context: if res == 'users': count_context[org_id]['admins'] = entry['admins'] count_context[org_id]['users'] = entry['users'] continue count_context[org_id][res] = entry['%s__count' % org_reference] # Combine the counts for job templates by project and inventory for org in org_id_list: org_id = org['id'] count_context[org_id]['job_templates'] = 0 for related_path in ['job_templates_project', 'job_templates_inventory']: if related_path in count_context[org_id]: count_context[org_id]['job_templates'] += count_context[org_id].pop(related_path) full_context['related_field_counts'] = count_context return full_context class OrganizationList(OrganizationCountsMixin, ListCreateAPIView): model = Organization serializer_class = OrganizationSerializer def get_queryset(self): qs = Organization.accessible_objects(self.request.user, 'read_role') qs = qs.select_related('admin_role', 'auditor_role', 'member_role', 'read_role') qs = qs.prefetch_related('created_by', 'modified_by') return qs def create(self, request, *args, **kwargs): """Create a new organzation. If there is already an organization and the license of the Tower instance does not permit multiple organizations, then raise LicenseForbids. """ # Sanity check: If the multiple organizations feature is disallowed # by the license, then we are only willing to create this organization # if no organizations exist in the system. if (not feature_enabled('multiple_organizations') and self.model.objects.exists()): raise LicenseForbids(_('Your Tower license only permits a single ' 'organization to exist.')) # Okay, create the organization as usual. return super(OrganizationList, self).create(request, *args, **kwargs) class OrganizationDetail(RetrieveUpdateDestroyAPIView): model = Organization serializer_class = OrganizationSerializer def get_serializer_context(self, *args, **kwargs): full_context = super(OrganizationDetail, self).get_serializer_context(*args, **kwargs) if not hasattr(self, 'kwargs'): return full_context org_id = int(self.kwargs['pk']) org_counts = {} access_kwargs = {'accessor': self.request.user, 'role_field': 'read_role'} direct_counts = Organization.objects.filter(id=org_id).annotate( users=Count('member_role__members', distinct=True), admins=Count('admin_role__members', distinct=True) ).values('users', 'admins') if not direct_counts: return full_context org_counts = direct_counts[0] org_counts['inventories'] = Inventory.accessible_objects(**access_kwargs).filter( organization__id=org_id).count() org_counts['teams'] = Team.accessible_objects(**access_kwargs).filter( organization__id=org_id).count() org_counts['projects'] = Project.accessible_objects(**access_kwargs).filter( organization__id=org_id).count() org_counts['job_templates'] = JobTemplate.accessible_objects(**access_kwargs).exclude( job_type='scan').filter(project__organization__id=org_id).count() org_counts['job_templates'] += JobTemplate.accessible_objects(**access_kwargs).filter( job_type='scan').filter(inventory__organization__id=org_id).count() full_context['related_field_counts'] = {} full_context['related_field_counts'][org_id] = org_counts return full_context class OrganizationInventoriesList(SubListAPIView): model = Inventory serializer_class = InventorySerializer parent_model = Organization relationship = 'inventories' class BaseUsersList(SubListCreateAttachDetachAPIView): def post(self, request, *args, **kwargs): ret = super(BaseUsersList, self).post( request, *args, **kwargs) try: if ret.data is not None and request.data.get('is_system_auditor', False): # This is a faux-field that just maps to checking the system # auditor role member list.. unfortunately this means we can't # set it on creation, and thus needs to be set here. user = User.objects.get(id=ret.data['id']) user.is_system_auditor = request.data['is_system_auditor'] ret.data['is_system_auditor'] = request.data['is_system_auditor'] except AttributeError as exc: print(exc) pass return ret class OrganizationUsersList(BaseUsersList): model = User serializer_class = UserSerializer parent_model = Organization relationship = 'member_role.members' class OrganizationAdminsList(BaseUsersList): model = User serializer_class = UserSerializer parent_model = Organization relationship = 'admin_role.members' class OrganizationProjectsList(SubListCreateAttachDetachAPIView): model = Project serializer_class = ProjectSerializer parent_model = Organization relationship = 'projects' parent_key = 'organization' class OrganizationWorkflowJobTemplatesList(SubListCreateAttachDetachAPIView): model = WorkflowJobTemplate serializer_class = WorkflowJobTemplateListSerializer parent_model = Organization relationship = 'workflows' parent_key = 'organization' new_in_310 = True class OrganizationTeamsList(SubListCreateAttachDetachAPIView): model = Team serializer_class = TeamSerializer parent_model = Organization relationship = 'teams' parent_key = 'organization' class OrganizationActivityStreamList(ActivityStreamEnforcementMixin, SubListAPIView): model = ActivityStream serializer_class = ActivityStreamSerializer parent_model = Organization relationship = 'activitystream_set' new_in_145 = True class OrganizationNotificationTemplatesList(SubListCreateAttachDetachAPIView): model = NotificationTemplate serializer_class = NotificationTemplateSerializer parent_model = Organization relationship = 'notification_templates' parent_key = 'organization' class OrganizationNotificationTemplatesAnyList(SubListCreateAttachDetachAPIView): model = NotificationTemplate serializer_class = NotificationTemplateSerializer parent_model = Organization relationship = 'notification_templates_any' new_in_300 = True class OrganizationNotificationTemplatesErrorList(SubListCreateAttachDetachAPIView): model = NotificationTemplate serializer_class = NotificationTemplateSerializer parent_model = Organization relationship = 'notification_templates_error' new_in_300 = True class OrganizationNotificationTemplatesSuccessList(SubListCreateAttachDetachAPIView): model = NotificationTemplate serializer_class = NotificationTemplateSerializer parent_model = Organization relationship = 'notification_templates_success' new_in_300 = True class OrganizationAccessList(ResourceAccessList): model = User # needs to be User for AccessLists's parent_model = Organization new_in_300 = True class OrganizationObjectRolesList(SubListAPIView): model = Role serializer_class = RoleSerializer parent_model = Organization new_in_300 = True def get_queryset(self): po = self.get_parent_object() content_type = ContentType.objects.get_for_model(self.parent_model) return Role.objects.filter(content_type=content_type, object_id=po.pk) class TeamList(ListCreateAPIView): model = Team serializer_class = TeamSerializer def get_queryset(self): qs = Team.accessible_objects(self.request.user, 'read_role').order_by() qs = qs.select_related('admin_role', 'read_role', 'member_role', 'organization') return qs class TeamDetail(RetrieveUpdateDestroyAPIView): model = Team serializer_class = TeamSerializer class TeamUsersList(BaseUsersList): model = User serializer_class = UserSerializer parent_model = Team relationship = 'member_role.members' class TeamRolesList(SubListCreateAttachDetachAPIView): model = Role serializer_class = RoleSerializerWithParentAccess metadata_class = RoleMetadata parent_model = Team relationship='member_role.children' new_in_300 = True def get_queryset(self): team = get_object_or_404(Team, pk=self.kwargs['pk']) if not self.request.user.can_access(Team, 'read', team): raise PermissionDenied() return Role.filter_visible_roles(self.request.user, team.member_role.children.all().exclude(pk=team.read_role.pk)) def post(self, request, *args, **kwargs): # Forbid implicit role creation here sub_id = request.data.get('id', None) if not sub_id: data = dict(msg=_("Role 'id' field is missing.")) return Response(data, status=status.HTTP_400_BAD_REQUEST) role = get_object_or_400(Role, pk=sub_id) org_content_type = ContentType.objects.get_for_model(Organization) if role.content_type == org_content_type: data = dict(msg=_("You cannot assign an Organization 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(Team, pk=self.kwargs['pk']) credential_content_type = ContentType.objects.get_for_model(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 = Role serializer_class = RoleSerializer parent_model = Team new_in_300 = True def get_queryset(self): po = self.get_parent_object() content_type = ContentType.objects.get_for_model(self.parent_model) return Role.objects.filter(content_type=content_type, object_id=po.pk) class TeamProjectsList(SubListAPIView): model = Project serializer_class = ProjectSerializer parent_model = 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 = 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(ActivityStreamEnforcementMixin, SubListAPIView): model = ActivityStream serializer_class = ActivityStreamSerializer parent_model = Team relationship = 'activitystream_set' new_in_145 = True 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=Project.accessible_objects(parent, 'read_role')) | Q(credential__in=Credential.accessible_objects(parent, 'read_role'))) class TeamAccessList(ResourceAccessList): model = User # needs to be User for AccessLists's parent_model = Team new_in_300 = True class ProjectList(ListCreateAPIView): model = Project serializer_class = ProjectSerializer capabilities_prefetch = ['admin', 'update'] def get_queryset(self): projects_qs = Project.accessible_objects(self.request.user, 'read_role') projects_qs = projects_qs.select_related( 'organization', 'admin_role', 'use_role', 'update_role', 'read_role', ) projects_qs = projects_qs.prefetch_related('last_job', 'created_by') return projects_qs class ProjectDetail(RetrieveUpdateDestroyAPIView): model = Project serializer_class = ProjectSerializer def destroy(self, request, *args, **kwargs): obj = self.get_object() can_delete = request.user.can_access(Project, 'delete', obj) if not can_delete: raise PermissionDenied(_("Cannot delete project.")) for pu in obj.project_updates.filter(status__in=['new', 'pending', 'waiting', 'running']): pu.cancel() return super(ProjectDetail, self).destroy(request, *args, **kwargs) class ProjectPlaybooks(RetrieveAPIView): model = Project serializer_class = ProjectPlaybooksSerializer class ProjectInventories(RetrieveAPIView): model = Project serializer_class = ProjectInventoriesSerializer class ProjectTeamsList(ListAPIView): model = Team serializer_class = TeamSerializer def get_queryset(self): p = get_object_or_404(Project, pk=self.kwargs['pk']) if not self.request.user.can_access(Project, 'read', p): raise PermissionDenied() project_ct = ContentType.objects.get_for_model(Project) team_ct = ContentType.objects.get_for_model(self.model) all_roles = 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): view_name = _("Project Schedules") model = Schedule serializer_class = ScheduleSerializer parent_model = Project relationship = 'schedules' parent_key = 'unified_job_template' new_in_148 = True class ProjectScmInventorySources(SubListCreateAPIView): view_name = _("Project SCM Inventory Sources") model = Inventory serializer_class = InventorySourceSerializer parent_model = Project relationship = 'scm_inventory_sources' parent_key = 'scm_project' new_in_320 = True class ProjectActivityStreamList(ActivityStreamEnforcementMixin, SubListAPIView): model = ActivityStream serializer_class = ActivityStreamSerializer parent_model = Project relationship = 'activitystream_set' new_in_145 = True 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 = NotificationTemplate serializer_class = NotificationTemplateSerializer parent_model = Project relationship = 'notification_templates_any' new_in_300 = True class ProjectNotificationTemplatesErrorList(SubListCreateAttachDetachAPIView): model = NotificationTemplate serializer_class = NotificationTemplateSerializer parent_model = Project relationship = 'notification_templates_error' new_in_300 = True class ProjectNotificationTemplatesSuccessList(SubListCreateAttachDetachAPIView): model = NotificationTemplate serializer_class = NotificationTemplateSerializer parent_model = Project relationship = 'notification_templates_success' new_in_300 = True class ProjectUpdatesList(SubListAPIView): model = ProjectUpdate serializer_class = ProjectUpdateSerializer parent_model = Project relationship = 'project_updates' new_in_13 = True class ProjectUpdateView(RetrieveAPIView): model = Project serializer_class = ProjectUpdateViewSerializer permission_classes = (ProjectUpdatePermission,) new_in_13 = True 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: headers = {'Location': project_update.get_absolute_url(request=request)} return Response({'project_update': project_update.id}, headers=headers, status=status.HTTP_202_ACCEPTED) else: return self.http_method_not_allowed(request, *args, **kwargs) class ProjectUpdateList(ListAPIView): model = ProjectUpdate serializer_class = ProjectUpdateListSerializer new_in_13 = True class ProjectUpdateDetail(RetrieveDestroyAPIView): model = ProjectUpdate serializer_class = ProjectUpdateSerializer new_in_13 = True def destroy(self, request, *args, **kwargs): obj = self.get_object() try: if obj.unified_job_node.workflow_job.status in ACTIVE_STATES: raise PermissionDenied(detail=_('Cannot delete job resource when associated workflow job is running.')) except ProjectUpdate.unified_job_node.RelatedObjectDoesNotExist: pass return super(ProjectUpdateDetail, self).destroy(request, *args, **kwargs) class ProjectUpdateCancel(RetrieveAPIView): model = ProjectUpdate serializer_class = ProjectUpdateCancelSerializer is_job_cancel = True new_in_13 = True 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 = Notification serializer_class = NotificationSerializer parent_model = ProjectUpdate relationship = 'notifications' new_in_300 = True class ProjectUpdateScmInventoryUpdates(SubListCreateAPIView): view_name = _("Project Update SCM Inventory Updates") model = InventoryUpdate serializer_class = InventoryUpdateSerializer parent_model = ProjectUpdate relationship = 'scm_inventory_updates' parent_key = 'scm_project_update' new_in_320 = True class ProjectAccessList(ResourceAccessList): model = User # needs to be User for AccessLists's parent_model = Project new_in_300 = True class ProjectObjectRolesList(SubListAPIView): model = Role serializer_class = RoleSerializer parent_model = Project new_in_300 = True def get_queryset(self): po = self.get_parent_object() content_type = ContentType.objects.get_for_model(self.parent_model) return Role.objects.filter(content_type=content_type, object_id=po.pk) class UserList(ListCreateAPIView): model = User serializer_class = UserSerializer permission_classes = (UserPermission,) def post(self, request, *args, **kwargs): ret = super(UserList, self).post( request, *args, **kwargs) try: if request.data.get('is_system_auditor', False): # This is a faux-field that just maps to checking the system # auditor role member list.. unfortunately this means we can't # set it on creation, and thus needs to be set here. user = User.objects.get(id=ret.data['id']) user.is_system_auditor = request.data['is_system_auditor'] ret.data['is_system_auditor'] = request.data['is_system_auditor'] except AttributeError as exc: print(exc) pass return ret class UserMeList(ListAPIView): model = User serializer_class = UserSerializer view_name = _('Me') def get_queryset(self): return self.model.objects.filter(pk=self.request.user.pk) class UserTeamsList(ListAPIView): model = User serializer_class = TeamSerializer def get_queryset(self): u = get_object_or_404(User, pk=self.kwargs['pk']) if not self.request.user.can_access(User, 'read', u): raise PermissionDenied() return Team.accessible_objects(self.request.user, 'read_role').filter( Q(member_role__members=u) | Q(admin_role__members=u)).distinct() class UserRolesList(SubListCreateAttachDetachAPIView): model = Role serializer_class = RoleSerializerWithParentAccess metadata_class = RoleMetadata parent_model = User relationship='roles' permission_classes = (IsAuthenticated,) new_in_300 = True def get_queryset(self): u = get_object_or_404(User, pk=self.kwargs['pk']) if not self.request.user.can_access(User, 'read', u): raise PermissionDenied() content_type = ContentType.objects.get_for_model(User) return 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): # Forbid implicit role creation here sub_id = request.data.get('id', None) if not sub_id: data = dict(msg=_("Role 'id' field is missing.")) return Response(data, status=status.HTTP_400_BAD_REQUEST) if sub_id == self.request.user.admin_role.pk: raise PermissionDenied(_('You may not perform any action with your own admin_role.')) user = get_object_or_400(User, pk=self.kwargs['pk']) role = get_object_or_400(Role, pk=sub_id) user_content_type = ContentType.objects.get_for_model(User) if role.content_type == user_content_type: raise PermissionDenied(_('You may not change the membership of a users admin_role')) credential_content_type = ContentType.objects.get_for_model(Credential) if role.content_type == credential_content_type: if 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 = Project serializer_class = ProjectSerializer parent_model = User def get_queryset(self): parent = self.get_parent_object() self.check_parent_access(parent) my_qs = Project.accessible_objects(self.request.user, 'read_role') user_qs = Project.accessible_objects(parent, 'read_role') return my_qs & user_qs class UserOrganizationsList(OrganizationCountsMixin, SubListAPIView): model = Organization serializer_class = OrganizationSerializer parent_model = User relationship = 'organizations' def get_queryset(self): parent = self.get_parent_object() self.check_parent_access(parent) my_qs = Organization.accessible_objects(self.request.user, 'read_role') user_qs = Organization.objects.filter(member_role__members=parent) return my_qs & user_qs class UserAdminOfOrganizationsList(OrganizationCountsMixin, SubListAPIView): model = Organization serializer_class = OrganizationSerializer parent_model = User relationship = 'admin_of_organizations' def get_queryset(self): parent = self.get_parent_object() self.check_parent_access(parent) my_qs = Organization.accessible_objects(self.request.user, 'read_role') user_qs = Organization.objects.filter(admin_role__members=parent) return my_qs & user_qs class UserActivityStreamList(ActivityStreamEnforcementMixin, SubListAPIView): model = ActivityStream serializer_class = ActivityStreamSerializer parent_model = User relationship = 'activitystream_set' new_in_145 = True 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 = User serializer_class = 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(User, 'change', obj, request.data) can_admin = request.user.can_access(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(User, 'delete', obj) if not can_delete: raise PermissionDenied(_('Cannot delete user.')) return super(UserDetail, self).destroy(request, *args, **kwargs) class UserAccessList(ResourceAccessList): model = User # needs to be User for AccessLists's parent_model = User new_in_300 = True class CredentialTypeList(ListCreateAPIView): model = CredentialType serializer_class = CredentialTypeSerializer new_in_320 = True new_in_api_v2 = True class CredentialTypeDetail(RetrieveUpdateDestroyAPIView): model = CredentialType serializer_class = CredentialTypeSerializer new_in_320 = True new_in_api_v2 = True class CredentialList(ListCreateAPIView): model = Credential serializer_class = CredentialSerializerCreate capabilities_prefetch = ['admin', 'use'] class CredentialOwnerUsersList(SubListAPIView): model = User serializer_class = UserSerializer parent_model = Credential relationship = 'admin_role.members' new_in_300 = True class CredentialOwnerTeamsList(SubListAPIView): model = Team serializer_class = TeamSerializer parent_model = Credential new_in_300 = True def get_queryset(self): credential = get_object_or_404(self.parent_model, pk=self.kwargs['pk']) if not self.request.user.can_access(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 = Credential serializer_class = UserCredentialSerializerCreate parent_model = User parent_key = 'user' def get_queryset(self): user = self.get_parent_object() self.check_parent_access(user) visible_creds = Credential.accessible_objects(self.request.user, 'read_role') user_creds = Credential.accessible_objects(user, 'read_role') return user_creds & visible_creds class TeamCredentialsList(SubListCreateAPIView): model = Credential serializer_class = TeamCredentialSerializerCreate parent_model = Team parent_key = 'team' def get_queryset(self): team = self.get_parent_object() self.check_parent_access(team) visible_creds = Credential.accessible_objects(self.request.user, 'read_role') team_creds = 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 = Credential serializer_class = OrganizationCredentialSerializerCreate parent_model = Organization parent_key = 'organization' def get_queryset(self): organization = self.get_parent_object() self.check_parent_access(organization) user_visible = Credential.accessible_objects(self.request.user, 'read_role').all() org_set = 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 = Credential serializer_class = CredentialSerializer class CredentialActivityStreamList(ActivityStreamEnforcementMixin, SubListAPIView): model = ActivityStream serializer_class = ActivityStreamSerializer parent_model = Credential relationship = 'activitystream_set' new_in_145 = True class CredentialAccessList(ResourceAccessList): model = User # needs to be User for AccessLists's parent_model = Credential new_in_300 = True class CredentialObjectRolesList(SubListAPIView): model = Role serializer_class = RoleSerializer parent_model = Credential new_in_300 = True def get_queryset(self): po = self.get_parent_object() content_type = ContentType.objects.get_for_model(self.parent_model) return Role.objects.filter(content_type=content_type, object_id=po.pk) class InventoryScriptList(ListCreateAPIView): model = CustomInventoryScript serializer_class = CustomInventoryScriptSerializer new_in_210 = True class InventoryScriptDetail(RetrieveUpdateDestroyAPIView): model = CustomInventoryScript serializer_class = CustomInventoryScriptSerializer new_in_210 = True def destroy(self, request, *args, **kwargs): instance = self.get_object() can_delete = request.user.can_access(self.model, 'delete', instance) if not can_delete: raise PermissionDenied(_("Cannot delete inventory script.")) for inv_src in InventorySource.objects.filter(source_script=instance): inv_src.source_script = None inv_src.save() return super(InventoryScriptDetail, self).destroy(request, *args, **kwargs) class InventoryScriptObjectRolesList(SubListAPIView): model = Role serializer_class = RoleSerializer parent_model = CustomInventoryScript new_in_300 = True def get_queryset(self): po = self.get_parent_object() content_type = ContentType.objects.get_for_model(self.parent_model) return Role.objects.filter(content_type=content_type, object_id=po.pk) class InventoryList(ListCreateAPIView): model = Inventory serializer_class = InventorySerializer capabilities_prefetch = ['admin', 'adhoc'] def get_queryset(self): qs = Inventory.accessible_objects(self.request.user, 'read_role') qs = qs.select_related('admin_role', 'read_role', 'update_role', 'use_role', 'adhoc_role') qs = qs.prefetch_related('created_by', 'modified_by', 'organization') return qs class InventoryDetail(RetrieveUpdateDestroyAPIView): model = Inventory serializer_class = InventoryDetailSerializer def destroy(self, request, *args, **kwargs): with ignore_inventory_computed_fields(): with ignore_inventory_group_removal(): return super(InventoryDetail, self).destroy(request, *args, **kwargs) class InventoryActivityStreamList(ActivityStreamEnforcementMixin, SubListAPIView): model = ActivityStream serializer_class = ActivityStreamSerializer parent_model = Inventory relationship = 'activitystream_set' new_in_145 = True 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(inventory=parent) | Q(host__in=parent.hosts.all()) | Q(group__in=parent.groups.all())) class InventoryAccessList(ResourceAccessList): model = User # needs to be User for AccessLists's parent_model = Inventory new_in_300 = True class InventoryObjectRolesList(SubListAPIView): model = Role serializer_class = RoleSerializer parent_model = Inventory new_in_300 = True def get_queryset(self): po = self.get_parent_object() content_type = ContentType.objects.get_for_model(self.parent_model) return Role.objects.filter(content_type=content_type, object_id=po.pk) class InventoryJobTemplateList(SubListAPIView): model = JobTemplate serializer_class = JobTemplateSerializer parent_model = Inventory relationship = 'jobtemplates' new_in_300 = True 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(inventory=parent) class InventoryScanJobTemplateList(SubListAPIView): model = JobTemplate serializer_class = JobTemplateSerializer parent_model = Inventory relationship = 'jobtemplates' new_in_220 = True 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(job_type=PERM_INVENTORY_SCAN, inventory=parent) class HostList(ListCreateAPIView): always_allow_superuser = False model = Host serializer_class = HostSerializer capabilities_prefetch = ['inventory.admin'] def get_queryset(self): qs = super(HostList, self).get_queryset() filter_string = self.request.query_params.get('host_filter', None) if filter_string: filter_q = DynamicFilterField.filter_string_to_q(filter_string) qs = qs.filter(filter_q) return qs def list(self, *args, **kwargs): try: return super(HostList, self).list(*args, **kwargs) except Exception as e: return Response(dict(error=_(unicode(e))), status=status.HTTP_400_BAD_REQUEST) class HostDetail(RetrieveUpdateDestroyAPIView): always_allow_superuser = False model = Host serializer_class = HostSerializer class HostAnsibleFactsDetail(RetrieveAPIView): model = Host serializer_class = AnsibleFactsSerializer new_in_320 = True new_in_api_v2 = True class InventoryHostsList(SubListCreateAttachDetachAPIView): model = Host serializer_class = HostSerializer parent_model = Inventory relationship = 'hosts' parent_key = 'inventory' class HostGroupsList(SubListCreateAttachDetachAPIView): ''' the list of groups a host is directly a member of ''' model = Group serializer_class = GroupSerializer parent_model = 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 = Group serializer_class = GroupSerializer parent_model = 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 = InventorySource serializer_class = InventorySourceSerializer parent_model = Host relationship = 'inventory_sources' new_in_148 = True class HostActivityStreamList(ActivityStreamEnforcementMixin, SubListAPIView): model = ActivityStream serializer_class = ActivityStreamSerializer parent_model = Host relationship = 'activitystream_set' new_in_145 = True 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 HostFactVersionsList(SystemTrackingEnforcementMixin, ParentMixin, ListAPIView): model = Fact serializer_class = FactVersionSerializer parent_model = Host new_in_220 = True def get_queryset(self): from_spec = self.request.query_params.get('from', None) to_spec = self.request.query_params.get('to', None) module_spec = self.request.query_params.get('module', None) if from_spec: from_spec = dateutil.parser.parse(from_spec) if to_spec: to_spec = dateutil.parser.parse(to_spec) host_obj = self.get_parent_object() return Fact.get_timeline(host_obj.id, module=module_spec, ts_from=from_spec, ts_to=to_spec) def list(self, *args, **kwargs): queryset = self.get_queryset() or [] return Response(dict(results=self.serializer_class(queryset, many=True).data)) class HostFactCompareView(SystemTrackingEnforcementMixin, SubDetailAPIView): model = Fact new_in_220 = True parent_model = Host serializer_class = FactSerializer def retrieve(self, request, *args, **kwargs): datetime_spec = request.query_params.get('datetime', None) module_spec = request.query_params.get('module', "ansible") datetime_actual = dateutil.parser.parse(datetime_spec) if datetime_spec is not None else now() host_obj = self.get_parent_object() fact_entry = Fact.get_host_fact(host_obj.id, module_spec, datetime_actual) if not fact_entry: return Response({'detail': _('Fact not found.')}, status=status.HTTP_404_NOT_FOUND) return Response(self.serializer_class(instance=fact_entry).data) class GroupList(ListCreateAPIView): model = Group serializer_class = GroupSerializer capabilities_prefetch = ['inventory.admin', 'inventory.adhoc'] 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/v1/parent_model/34/child_model. If you then wanted a child of the child model you would NOT do /api/v1/parent_model/34/child_model/87/child_child_model Instead, you would access the child_child_model via /api/v1/child_child_model/87/ and you would create child_child_model's off of /api/v1/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(EnforceParentRelationshipMixin, SubListCreateAttachDetachAPIView): model = Group serializer_class = GroupSerializer parent_model = 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) class GroupPotentialChildrenList(SubListAPIView): model = Group serializer_class = GroupSerializer parent_model = Group new_in_14 = True 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(SubListCreateAttachDetachAPIView): ''' the list of hosts directly below a group ''' model = Host serializer_class = HostSerializer parent_model = 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 = 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 = Host.objects.filter(inventory=parent_group.inventory, name=request.data['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(SubListAPIView): ''' the list of all hosts below a group, even including subgroups ''' model = Host serializer_class = HostSerializer parent_model = 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 = InventorySource serializer_class = InventorySourceSerializer parent_model = Group relationship = 'inventory_sources' new_in_148 = True class GroupActivityStreamList(ActivityStreamEnforcementMixin, SubListAPIView): model = ActivityStream serializer_class = ActivityStreamSerializer parent_model = Group relationship = 'activitystream_set' new_in_145 = True 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(RetrieveUpdateDestroyAPIView): model = Group serializer_class = GroupSerializer def destroy(self, request, *args, **kwargs): obj = self.get_object() if not request.user.can_access(self.model, 'delete', obj): raise PermissionDenied() if get_request_version(request) == 1: # TODO: deletion of automatic inventory_source, remove in 3.3 try: obj.deprecated_inventory_source.delete() except Group.deprecated_inventory_source.RelatedObjectDoesNotExist: pass obj.delete_recursive() return Response(status=status.HTTP_204_NO_CONTENT) class InventoryGroupsList(SubListCreateAttachDetachAPIView): model = Group serializer_class = GroupSerializer parent_model = Inventory relationship = 'groups' parent_key = 'inventory' class InventoryRootGroupsList(SubListCreateAttachDetachAPIView): model = Group serializer_class = GroupSerializer parent_model = 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] is_variable_data = True # Special flag for permissions check. class InventoryVariableData(BaseVariableData): model = Inventory serializer_class = InventoryVariableDataSerializer class HostVariableData(BaseVariableData): model = Host serializer_class = HostVariableDataSerializer class GroupVariableData(BaseVariableData): model = Group serializer_class = GroupVariableDataSerializer class InventoryScriptView(RetrieveAPIView): model = Inventory serializer_class = InventoryScriptSerializer authentication_classes = [TaskAuthentication] + api_settings.DEFAULT_AUTHENTICATION_CLASSES 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', '')) show_all = bool(request.query_params.get('all', '')) if show_all: hosts_q = dict() else: hosts_q = dict(enabled=True) if hostname: host = get_object_or_404(obj.hosts, name=hostname, **hosts_q) data = host.variables_dict else: data = OrderedDict() if obj.variables_dict: all_group = data.setdefault('all', OrderedDict()) all_group['vars'] = obj.variables_dict # Add hosts without a group to the all group. groupless_hosts_qs = obj.hosts.filter(groups__isnull=True, **hosts_q).order_by('name') groupless_hosts = list(groupless_hosts_qs.values_list('name', flat=True)) if groupless_hosts: all_group = data.setdefault('all', OrderedDict()) all_group['hosts'] = groupless_hosts # Build in-memory mapping of groups and their hosts. group_hosts_kw = dict(group__inventory_id=obj.id, host__inventory_id=obj.id) if 'enabled' in hosts_q: group_hosts_kw['host__enabled'] = hosts_q['enabled'] group_hosts_qs = Group.hosts.through.objects.filter(**group_hosts_kw) group_hosts_qs = group_hosts_qs.order_by('host__name') group_hosts_qs = group_hosts_qs.values_list('group_id', 'host_id', 'host__name') group_hosts_map = {} for group_id, host_id, host_name in group_hosts_qs: group_hostnames = group_hosts_map.setdefault(group_id, []) group_hostnames.append(host_name) # Build in-memory mapping of groups and their children. group_parents_qs = Group.parents.through.objects.filter( from_group__inventory_id=obj.id, to_group__inventory_id=obj.id, ) group_parents_qs = group_parents_qs.order_by('from_group__name') group_parents_qs = group_parents_qs.values_list('from_group_id', 'from_group__name', 'to_group_id') group_children_map = {} for from_group_id, from_group_name, to_group_id in group_parents_qs: group_children = group_children_map.setdefault(to_group_id, []) group_children.append(from_group_name) # Now use in-memory maps to build up group info. for group in obj.groups.all(): group_info = OrderedDict() group_info['hosts'] = group_hosts_map.get(group.id, []) group_info['children'] = group_children_map.get(group.id, []) group_info['vars'] = group.variables_dict data[group.name] = group_info if hostvars: data.setdefault('_meta', OrderedDict()) data['_meta'].setdefault('hostvars', OrderedDict()) for host in obj.hosts.filter(**hosts_q): data['_meta']['hostvars'][host.name] = host.variables_dict # workaround for Ansible inventory bug (github #3687), localhost # must be explicitly listed in the all group for dynamic inventory # scripts to pick it up. localhost_names = ('localhost', '127.0.0.1', '::1') localhosts_qs = obj.hosts.filter(name__in=localhost_names, **hosts_q) localhosts = list(localhosts_qs.values_list('name', flat=True)) if localhosts: all_group = data.setdefault('all', OrderedDict()) all_group_hosts = all_group.get('hosts', []) all_group_hosts.extend(localhosts) all_group['hosts'] = sorted(set(all_group_hosts)) return Response(data) class InventoryTreeView(RetrieveAPIView): model = Inventory serializer_class = GroupTreeSerializer filter_backends = () new_in_13 = True 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 = 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): view_name = _('Inventory Source List') model = InventorySource serializer_class = InventorySourceSerializer parent_model = Inventory relationship = 'inventory_sources' parent_key = 'inventory' new_in_14 = True class InventorySourceList(ListCreateAPIView): model = InventorySource serializer_class = InventorySourceSerializer new_in_14 = True class InventorySourceDetail(RetrieveUpdateDestroyAPIView): model = InventorySource serializer_class = InventorySourceSerializer new_in_14 = True def destroy(self, request, *args, **kwargs): obj = self.get_object() can_delete = request.user.can_access(InventorySource, 'delete', obj) if not can_delete: raise PermissionDenied(_("Cannot delete inventory source.")) for pu in obj.inventory_updates.filter(status__in=['new', 'pending', 'waiting', 'running']): pu.cancel() return super(InventorySourceDetail, self).destroy(request, *args, **kwargs) class InventorySourceSchedulesList(SubListCreateAPIView): view_name = _("Inventory Source Schedules") model = Schedule serializer_class = ScheduleSerializer parent_model = InventorySource relationship = 'schedules' parent_key = 'unified_job_template' new_in_148 = True class InventorySourceActivityStreamList(ActivityStreamEnforcementMixin, SubListAPIView): model = ActivityStream serializer_class = ActivityStreamSerializer parent_model = InventorySource relationship = 'activitystream_set' new_in_145 = True class InventorySourceNotificationTemplatesAnyList(SubListCreateAttachDetachAPIView): model = NotificationTemplate serializer_class = NotificationTemplateSerializer parent_model = InventorySource relationship = 'notification_templates_any' new_in_300 = True def post(self, request, *args, **kwargs): parent = self.get_parent_object() if parent.source not in CLOUD_INVENTORY_SOURCES: return Response(dict(msg=_("Notification Templates can only be assigned when source is one of {}.") .format(CLOUD_INVENTORY_SOURCES, parent.source)), status=status.HTTP_400_BAD_REQUEST) return super(InventorySourceNotificationTemplatesAnyList, self).post(request, *args, **kwargs) class InventorySourceNotificationTemplatesErrorList(InventorySourceNotificationTemplatesAnyList): relationship = 'notification_templates_error' class InventorySourceNotificationTemplatesSuccessList(InventorySourceNotificationTemplatesAnyList): relationship = 'notification_templates_success' class InventorySourceHostsList(SubListAPIView): model = Host serializer_class = HostSerializer parent_model = InventorySource relationship = 'hosts' new_in_148 = True class InventorySourceGroupsList(SubListAPIView): model = Group serializer_class = GroupSerializer parent_model = InventorySource relationship = 'groups' new_in_148 = True class InventorySourceUpdatesList(SubListAPIView): model = InventoryUpdate serializer_class = InventoryUpdateSerializer parent_model = InventorySource relationship = 'inventory_updates' new_in_14 = True class InventorySourceUpdateView(RetrieveAPIView): model = InventorySource serializer_class = InventorySourceUpdateSerializer is_job_start = True new_in_14 = True def post(self, request, *args, **kwargs): obj = self.get_object() if obj.source == 'file' and obj.scm_project_id is not None: raise PermissionDenied(detail=_( 'Update the project `{}` in order to update this inventory source.'.format( obj.scm_project.name))) if obj.can_update: inventory_update = obj.update() if not inventory_update: return Response({}, status=status.HTTP_400_BAD_REQUEST) else: headers = {'Location': inventory_update.get_absolute_url(request=request)} return Response(dict(inventory_update=inventory_update.id), status=status.HTTP_202_ACCEPTED, headers=headers) else: return self.http_method_not_allowed(request, *args, **kwargs) class InventoryUpdateList(ListAPIView): model = InventoryUpdate serializer_class = InventoryUpdateListSerializer class InventoryUpdateDetail(RetrieveDestroyAPIView): model = InventoryUpdate serializer_class = InventoryUpdateSerializer new_in_14 = True def destroy(self, request, *args, **kwargs): obj = self.get_object() try: if obj.unified_job_node.workflow_job.status in ACTIVE_STATES: raise PermissionDenied(detail=_('Cannot delete job resource when associated workflow job is running.')) except InventoryUpdate.unified_job_node.RelatedObjectDoesNotExist: pass return super(InventoryUpdateDetail, self).destroy(request, *args, **kwargs) class InventoryUpdateCancel(RetrieveAPIView): model = InventoryUpdate serializer_class = InventoryUpdateCancelSerializer is_job_cancel = True new_in_14 = True 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 = Notification serializer_class = NotificationSerializer parent_model = InventoryUpdate relationship = 'notifications' new_in_300 = True class JobTemplateList(ListCreateAPIView): model = JobTemplate serializer_class = JobTemplateSerializer always_allow_superuser = False capabilities_prefetch = [ 'admin', 'execute', {'copy': ['project.use', 'inventory.use', 'credential.use', 'cloud_credential.use', 'network_credential.use']} ] def post(self, request, *args, **kwargs): ret = super(JobTemplateList, self).post(request, *args, **kwargs) if ret.status_code == 201: job_template = JobTemplate.objects.get(id=ret.data['id']) job_template.admin_role.members.add(request.user) return ret class JobTemplateDetail(RetrieveUpdateDestroyAPIView): model = JobTemplate serializer_class = JobTemplateSerializer always_allow_superuser = False class JobTemplateLaunch(RetrieveAPIView, GenericAPIView): model = JobTemplate serializer_class = JobLaunchSerializer is_job_start = True 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 p in obj.passwords_needed_to_start: data[p] = u'' for v in obj.variables_needed_to_start: extra_vars.setdefault(v, u'') if extra_vars: data['extra_vars'] = extra_vars ask_for_vars_dict = obj._ask_for_vars_dict() ask_for_vars_dict.pop('extra_vars') for field in ask_for_vars_dict: if not ask_for_vars_dict[field]: data.pop(field, None) elif field == 'inventory' or field == 'credential': data[field] = getattrd(obj, "%s.%s" % (field, 'id'), None) else: data[field] = getattr(obj, field) return data def post(self, request, *args, **kwargs): obj = self.get_object() if 'credential' not in request.data and 'credential_id' in request.data: request.data['credential'] = request.data['credential_id'] if 'inventory' not in request.data and 'inventory_id' in request.data: request.data['inventory'] = request.data['inventory_id'] passwords = {} serializer = self.serializer_class(instance=obj, data=request.data, context={'obj': obj, 'data': request.data, 'passwords': passwords}) if not serializer.is_valid(): return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) prompted_fields, ignored_fields = obj._accept_or_ignore_job_kwargs(**request.data) if 'credential' in prompted_fields and prompted_fields['credential'] != getattrd(obj, 'credential.pk', None): new_credential = get_object_or_400(Credential, pk=get_pk_from_dict(prompted_fields, 'credential')) if request.user not in new_credential.use_role: raise PermissionDenied() if 'inventory' in prompted_fields and prompted_fields['inventory'] != getattrd(obj, 'inventory.pk', None): new_inventory = get_object_or_400(Inventory, pk=get_pk_from_dict(prompted_fields, 'inventory')) if request.user not in new_inventory.use_role: raise PermissionDenied() new_job = obj.create_unified_job(**prompted_fields) 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() data['ignored_fields'] = ignored_fields data.update(JobSerializer(new_job, context=self.get_serializer_context()).to_representation(new_job)) data['job'] = new_job.id return Response(data, status=status.HTTP_201_CREATED) class JobTemplateSchedulesList(SubListCreateAPIView): view_name = _("Job Template Schedules") model = Schedule serializer_class = ScheduleSerializer parent_model = JobTemplate relationship = 'schedules' parent_key = 'unified_job_template' new_in_148 = True class JobTemplateSurveySpec(GenericAPIView): model = JobTemplate parent_model = JobTemplate serializer_class = EmptySerializer new_in_210 = True def get(self, request, *args, **kwargs): obj = self.get_object() if not feature_enabled('surveys'): raise LicenseForbids(_('Your license does not allow ' 'adding surveys.')) survey_spec = obj.survey_spec for pos, field in enumerate(survey_spec.get('spec', [])): if field.get('type') == 'password': if 'default' in field and field['default']: field['default'] = '$encrypted$' return Response(survey_spec) def post(self, request, *args, **kwargs): obj = self.get_object() # Sanity check: Are surveys available on this license? # If not, do not allow them to be used. if not feature_enabled('surveys'): raise LicenseForbids(_('Your license does not allow ' 'adding surveys.')) if not request.user.can_access(self.model, 'change', obj, None): raise PermissionDenied() new_spec = request.data if "name" not in new_spec: return Response(dict(error=_("'name' missing from survey spec.")), status=status.HTTP_400_BAD_REQUEST) if "description" not in new_spec: return Response(dict(error=_("'description' missing from survey spec.")), status=status.HTTP_400_BAD_REQUEST) if "spec" not in new_spec: return Response(dict(error=_("'spec' missing from survey spec.")), status=status.HTTP_400_BAD_REQUEST) if not isinstance(new_spec["spec"], list): return Response(dict(error=_("'spec' must be a list of items.")), status=status.HTTP_400_BAD_REQUEST) if len(new_spec["spec"]) < 1: return Response(dict(error=_("'spec' doesn't contain any items.")), status=status.HTTP_400_BAD_REQUEST) idx = 0 variable_set = set() for survey_item in new_spec["spec"]: 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) if "type" not in survey_item: return Response(dict(error=_("'type' missing from survey question %s.") % str(idx)), status=status.HTTP_400_BAD_REQUEST) if "question_name" not in survey_item: return Response(dict(error=_("'question_name' missing from survey question %s.") % str(idx)), status=status.HTTP_400_BAD_REQUEST) if "variable" not in survey_item: return Response(dict(error=_("'variable' missing from survey question %s.") % str(idx)), 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']) if "required" not in survey_item: return Response(dict(error=_("'required' missing from survey question %s.") % str(idx)), status=status.HTTP_400_BAD_REQUEST) if survey_item["type"] == "password": if survey_item.get("default") and survey_item["default"].startswith('$encrypted$'): old_spec = obj.survey_spec for old_item in old_spec['spec']: if old_item['variable'] == survey_item['variable']: survey_item['default'] = old_item['default'] idx += 1 obj.survey_spec = new_spec obj.save(update_fields=['survey_spec']) return Response() 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(WorkflowsEnforcementMixin, JobTemplateSurveySpec): model = WorkflowJobTemplate parent_model = WorkflowJobTemplate new_in_310 = True class JobTemplateActivityStreamList(ActivityStreamEnforcementMixin, SubListAPIView): model = ActivityStream serializer_class = ActivityStreamSerializer parent_model = JobTemplate relationship = 'activitystream_set' new_in_145 = True class JobTemplateNotificationTemplatesAnyList(SubListCreateAttachDetachAPIView): model = NotificationTemplate serializer_class = NotificationTemplateSerializer parent_model = JobTemplate relationship = 'notification_templates_any' new_in_300 = True class JobTemplateNotificationTemplatesErrorList(SubListCreateAttachDetachAPIView): model = NotificationTemplate serializer_class = NotificationTemplateSerializer parent_model = JobTemplate relationship = 'notification_templates_error' new_in_300 = True class JobTemplateNotificationTemplatesSuccessList(SubListCreateAttachDetachAPIView): model = NotificationTemplate serializer_class = NotificationTemplateSerializer parent_model = JobTemplate relationship = 'notification_templates_success' new_in_300 = True class JobTemplateLabelList(DeleteLastUnattachLabelMixin, SubListCreateAttachDetachAPIView): model = Label serializer_class = LabelSerializer parent_model = JobTemplate relationship = 'labels' new_in_300 = True 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 = 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 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 = JobTemplate permission_classes = (JobTemplateCallbackPermission,) serializer_class = 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() # First try for an exact match on the name. try: return set([hosts.get(name__in=remote_hosts)]) except (Host.DoesNotExist, Host.MultipleObjectsReturned): pass # Next, try matching based on name or ansible_host variables. matches = set() for host in hosts: for host_var in ['ansible_ssh_host', 'ansible_host']: ansible_host = host.variables_dict.get(host_var, '') if ansible_host in remote_hosts: matches.add(host) if host.name != ansible_host and host.name in remote_hosts: matches.add(host) if len(matches) == 1: return matches # Try to resolve forward addresses for each host to find matches. for host in hosts: hostnames = set([host.name]) for host_var in ['ansible_ssh_host', 'ansible_host']: ansible_host = host.variables_dict.get(host_var, '') if ansible_host: hostnames.add(ansible_host) for hostname in hostnames: try: result = socket.getaddrinfo(hostname, None) possible_ips = set(x[4][0] for x in result) possible_ips.discard(hostname) if possible_ips and possible_ips & remote_hosts: matches.add(host) except socket.gaierror: pass # Return all matches found. 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 '; 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(launch_type='callback') inventory_update.signal_start() inventory_update_pks.add(inventory_update.pk) inventory_update_qs = 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 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 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, "launch_type": 'callback'} if extra_vars is not None and job_template.ask_variables_on_launch: kv['extra_vars'] = callback_filter_out_ansible_extra_vars(extra_vars) with transaction.atomic(): job = job_template.create_job(**kv) # Send a signal to celery 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!')) 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(SubListCreateAPIView): model = Job serializer_class = JobListSerializer parent_model = JobTemplate relationship = 'jobs' parent_key = 'job_template' class JobTemplateAccessList(ResourceAccessList): model = User # needs to be User for AccessLists's parent_model = JobTemplate new_in_300 = True class JobTemplateObjectRolesList(SubListAPIView): model = Role serializer_class = RoleSerializer parent_model = JobTemplate new_in_300 = True def get_queryset(self): po = self.get_parent_object() content_type = ContentType.objects.get_for_model(self.parent_model) return Role.objects.filter(content_type=content_type, object_id=po.pk) class WorkflowJobNodeList(WorkflowsEnforcementMixin, ListAPIView): model = WorkflowJobNode serializer_class = WorkflowJobNodeListSerializer new_in_310 = True class WorkflowJobNodeDetail(WorkflowsEnforcementMixin, RetrieveAPIView): model = WorkflowJobNode serializer_class = WorkflowJobNodeDetailSerializer new_in_310 = True class WorkflowJobTemplateNodeList(WorkflowsEnforcementMixin, ListCreateAPIView): model = WorkflowJobTemplateNode serializer_class = WorkflowJobTemplateNodeListSerializer new_in_310 = True class WorkflowJobTemplateNodeDetail(WorkflowsEnforcementMixin, RetrieveUpdateDestroyAPIView): model = WorkflowJobTemplateNode serializer_class = WorkflowJobTemplateNodeDetailSerializer new_in_310 = True def update_raw_data(self, data): for fd in ['job_type', 'job_tags', 'skip_tags', 'limit', 'skip_tags']: data[fd] = None try: obj = self.get_object() data.update(obj.char_prompts) except: pass return super(WorkflowJobTemplateNodeDetail, self).update_raw_data(data) class WorkflowJobTemplateNodeChildrenBaseList(WorkflowsEnforcementMixin, EnforceParentRelationshipMixin, SubListCreateAttachDetachAPIView): model = WorkflowJobTemplateNode serializer_class = WorkflowJobTemplateNodeListSerializer always_allow_superuser = True parent_model = WorkflowJobTemplateNode relationship = '' enforce_parent_relationship = 'workflow_job_template' new_in_310 = True ''' 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): mutex_list = ('success_nodes', 'failure_nodes') if self.relationship == 'always_nodes' else ('always_nodes',) for relation in mutex_list: if getattr(parent, relation).all().exists(): return {'Error': 'Cannot associate {0} when {1} have been associated.'.format(self.relationship, relation)} if created: return None workflow_nodes = parent.workflow_job_template.workflow_job_template_nodes.all().\ prefetch_related('success_nodes', 'failure_nodes', 'always_nodes') graph = {} for workflow_node in workflow_nodes: graph[workflow_node.pk] = dict(node_object=workflow_node, metadata={'parent': None, 'traversed': False}) find = False for node_type in ['success_nodes', 'failure_nodes', 'always_nodes']: for workflow_node in workflow_nodes: parent_node = graph[workflow_node.pk] related_nodes = getattr(parent_node['node_object'], node_type).all() for related_node in related_nodes: sub_node = graph[related_node.pk] sub_node['metadata']['parent'] = parent_node if not find and parent == workflow_node and sub == related_node and self.relationship == node_type: find = True if not find: sub_node = graph[sub.pk] parent_node = graph[parent.pk] if sub_node['metadata']['parent'] is not None: return {"Error": "Multiple parent relationship not allowed."} sub_node['metadata']['parent'] = parent_node iter_node = sub_node while iter_node is not None: if iter_node['metadata']['traversed']: return {"Error": "Cycle detected."} iter_node['metadata']['traversed'] = True iter_node = iter_node['metadata']['parent'] return None class WorkflowJobTemplateNodeSuccessNodesList(WorkflowJobTemplateNodeChildrenBaseList): relationship = 'success_nodes' class WorkflowJobTemplateNodeFailureNodesList(WorkflowJobTemplateNodeChildrenBaseList): relationship = 'failure_nodes' class WorkflowJobTemplateNodeAlwaysNodesList(WorkflowJobTemplateNodeChildrenBaseList): relationship = 'always_nodes' class WorkflowJobNodeChildrenBaseList(WorkflowsEnforcementMixin, SubListAPIView): model = WorkflowJobNode serializer_class = WorkflowJobNodeListSerializer parent_model = WorkflowJobNode relationship = '' new_in_310 = True # #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(WorkflowsEnforcementMixin, ListCreateAPIView): model = WorkflowJobTemplate serializer_class = WorkflowJobTemplateListSerializer always_allow_superuser = False new_in_310 = True class WorkflowJobTemplateDetail(WorkflowsEnforcementMixin, RetrieveUpdateDestroyAPIView): model = WorkflowJobTemplate serializer_class = WorkflowJobTemplateSerializer always_allow_superuser = False new_in_310 = True class WorkflowJobTemplateCopy(WorkflowsEnforcementMixin, GenericAPIView): model = WorkflowJobTemplate parent_model = WorkflowJobTemplate serializer_class = EmptySerializer new_in_310 = True def get(self, request, *args, **kwargs): obj = self.get_object() 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 post(self, request, *args, **kwargs): obj = self.get_object() if not request.user.can_access(self.model, 'copy', obj): raise PermissionDenied() new_obj = obj.user_copy(request.user) if request.user not in new_obj.admin_role: new_obj.admin_role.members.add(request.user) data = OrderedDict() data.update(WorkflowJobTemplateSerializer( new_obj, context=self.get_serializer_context()).to_representation(new_obj)) return Response(data, status=status.HTTP_201_CREATED) class WorkflowJobTemplateLabelList(WorkflowsEnforcementMixin, JobTemplateLabelList): parent_model = WorkflowJobTemplate new_in_310 = True class WorkflowJobTemplateLaunch(WorkflowsEnforcementMixin, RetrieveAPIView): model = WorkflowJobTemplate serializer_class = WorkflowJobLaunchSerializer new_in_310 = True is_job_start = True 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 return data def post(self, request, *args, **kwargs): obj = self.get_object() if not request.user.can_access(self.model, 'start', obj): raise PermissionDenied() serializer = self.serializer_class(instance=obj, data=request.data) if not serializer.is_valid(): return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) prompted_fields, ignored_fields = obj._accept_or_ignore_job_kwargs(**request.data) new_job = obj.create_unified_job(**prompted_fields) new_job.signal_start() data = OrderedDict() data['ignored_fields'] = ignored_fields data.update(WorkflowJobSerializer(new_job, context=self.get_serializer_context()).to_representation(new_job)) data['workflow_job'] = new_job.id return Response(data, status=status.HTTP_201_CREATED) class WorkflowJobRelaunch(WorkflowsEnforcementMixin, GenericAPIView): model = WorkflowJob serializer_class = EmptySerializer is_job_start = True new_in_310 = True 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() new_workflow_job = obj.create_relaunch_workflow_job() new_workflow_job.signal_start() data = 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(WorkflowsEnforcementMixin, SubListCreateAPIView): model = WorkflowJobTemplateNode serializer_class = WorkflowJobTemplateNodeListSerializer parent_model = WorkflowJobTemplate relationship = 'workflow_job_template_nodes' parent_key = 'workflow_job_template' new_in_310 = True def update_raw_data(self, data): for fd in ['job_type', 'job_tags', 'skip_tags', 'limit', 'skip_tags']: data[fd] = None return super(WorkflowJobTemplateWorkflowNodesList, self).update_raw_data(data) def get_queryset(self): return super(WorkflowJobTemplateWorkflowNodesList, self).get_queryset().order_by('id') class WorkflowJobTemplateJobsList(WorkflowsEnforcementMixin, SubListAPIView): model = WorkflowJob serializer_class = WorkflowJobListSerializer parent_model = WorkflowJobTemplate relationship = 'workflow_jobs' parent_key = 'workflow_job_template' new_in_310 = True class WorkflowJobTemplateSchedulesList(WorkflowsEnforcementMixin, SubListCreateAPIView): view_name = _("Workflow Job Template Schedules") model = Schedule serializer_class = ScheduleSerializer parent_model = WorkflowJobTemplate relationship = 'schedules' parent_key = 'unified_job_template' new_in_310 = True class WorkflowJobTemplateNotificationTemplatesAnyList(WorkflowsEnforcementMixin, SubListCreateAttachDetachAPIView): model = NotificationTemplate serializer_class = NotificationTemplateSerializer parent_model = WorkflowJobTemplate relationship = 'notification_templates_any' new_in_310 = True class WorkflowJobTemplateNotificationTemplatesErrorList(WorkflowsEnforcementMixin, SubListCreateAttachDetachAPIView): model = NotificationTemplate serializer_class = NotificationTemplateSerializer parent_model = WorkflowJobTemplate relationship = 'notification_templates_error' new_in_310 = True class WorkflowJobTemplateNotificationTemplatesSuccessList(WorkflowsEnforcementMixin, SubListCreateAttachDetachAPIView): model = NotificationTemplate serializer_class = NotificationTemplateSerializer parent_model = WorkflowJobTemplate relationship = 'notification_templates_success' new_in_310 = True class WorkflowJobTemplateAccessList(WorkflowsEnforcementMixin, ResourceAccessList): model = User # needs to be User for AccessLists's parent_model = WorkflowJobTemplate new_in_310 = True class WorkflowJobTemplateObjectRolesList(WorkflowsEnforcementMixin, SubListAPIView): model = Role serializer_class = RoleSerializer parent_model = WorkflowJobTemplate new_in_310 = True def get_queryset(self): po = self.get_parent_object() content_type = ContentType.objects.get_for_model(self.parent_model) return Role.objects.filter(content_type=content_type, object_id=po.pk) class WorkflowJobTemplateActivityStreamList(WorkflowsEnforcementMixin, ActivityStreamEnforcementMixin, SubListAPIView): model = ActivityStream serializer_class = ActivityStreamSerializer parent_model = WorkflowJobTemplate relationship = 'activitystream_set' new_in_310 = True 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(WorkflowsEnforcementMixin, ListCreateAPIView): model = WorkflowJob serializer_class = WorkflowJobListSerializer new_in_310 = True class WorkflowJobDetail(WorkflowsEnforcementMixin, RetrieveDestroyAPIView): model = WorkflowJob serializer_class = WorkflowJobSerializer new_in_310 = True class WorkflowJobWorkflowNodesList(WorkflowsEnforcementMixin, SubListAPIView): model = WorkflowJobNode serializer_class = WorkflowJobNodeListSerializer always_allow_superuser = True parent_model = WorkflowJob relationship = 'workflow_job_nodes' parent_key = 'workflow_job' new_in_310 = True def get_queryset(self): return super(WorkflowJobWorkflowNodesList, self).get_queryset().order_by('id') class WorkflowJobCancel(WorkflowsEnforcementMixin, RetrieveAPIView): model = WorkflowJob serializer_class = WorkflowJobCancelSerializer is_job_cancel = True new_in_310 = True def post(self, request, *args, **kwargs): obj = self.get_object() if obj.can_cancel: obj.cancel() #TODO: Figure out whether an immediate schedule is needed. run_job_complete.delay(obj.id) return Response(status=status.HTTP_202_ACCEPTED) else: return self.http_method_not_allowed(request, *args, **kwargs) class WorkflowJobNotificationsList(WorkflowsEnforcementMixin, SubListAPIView): model = Notification serializer_class = NotificationSerializer parent_model = WorkflowJob relationship = 'notifications' new_in_310 = True class WorkflowJobActivityStreamList(WorkflowsEnforcementMixin, ActivityStreamEnforcementMixin, SubListAPIView): model = ActivityStream serializer_class = ActivityStreamSerializer parent_model = WorkflowJob relationship = 'activitystream_set' new_in_310 = True class SystemJobTemplateList(ListAPIView): model = SystemJobTemplate serializer_class = SystemJobTemplateSerializer new_in_210 = True 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 = SystemJobTemplate serializer_class = SystemJobTemplateSerializer new_in_210 = True class SystemJobTemplateLaunch(GenericAPIView): model = SystemJobTemplate serializer_class = EmptySerializer is_job_start = True new_in_210 = True 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 = dict(system_job=new_job.id) return Response(data, status=status.HTTP_201_CREATED) class SystemJobTemplateSchedulesList(SubListCreateAPIView): view_name = _("System Job Template Schedules") model = Schedule serializer_class = ScheduleSerializer parent_model = SystemJobTemplate relationship = 'schedules' parent_key = 'unified_job_template' new_in_210 = True class SystemJobTemplateJobsList(SubListAPIView): model = SystemJob serializer_class = SystemJobListSerializer parent_model = SystemJobTemplate relationship = 'jobs' parent_key = 'system_job_template' new_in_210 = True class SystemJobTemplateNotificationTemplatesAnyList(SubListCreateAttachDetachAPIView): model = NotificationTemplate serializer_class = NotificationTemplateSerializer parent_model = SystemJobTemplate relationship = 'notification_templates_any' new_in_300 = True class SystemJobTemplateNotificationTemplatesErrorList(SubListCreateAttachDetachAPIView): model = NotificationTemplate serializer_class = NotificationTemplateSerializer parent_model = SystemJobTemplate relationship = 'notification_templates_error' new_in_300 = True class SystemJobTemplateNotificationTemplatesSuccessList(SubListCreateAttachDetachAPIView): model = NotificationTemplate serializer_class = NotificationTemplateSerializer parent_model = SystemJobTemplate relationship = 'notification_templates_success' new_in_300 = True class JobList(ListCreateAPIView): model = Job serializer_class = JobListSerializer class JobDetail(RetrieveUpdateDestroyAPIView): model = Job serializer_class = JobSerializer 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) def destroy(self, request, *args, **kwargs): obj = self.get_object() try: if obj.unified_job_node.workflow_job.status in ACTIVE_STATES: raise PermissionDenied(detail=_('Cannot delete job resource when associated workflow job is running.')) except Job.unified_job_node.RelatedObjectDoesNotExist: pass return super(JobDetail, self).destroy(request, *args, **kwargs) class JobLabelList(SubListAPIView): model = Label serializer_class = LabelSerializer parent_model = Job relationship = 'labels' parent_key = 'job' new_in_300 = True class WorkflowJobLabelList(WorkflowsEnforcementMixin, JobLabelList): parent_model = WorkflowJob new_in_310 = True class JobActivityStreamList(ActivityStreamEnforcementMixin, SubListAPIView): model = ActivityStream serializer_class = ActivityStreamSerializer parent_model = Job relationship = 'activitystream_set' new_in_145 = True class JobStart(GenericAPIView): model = Job serializer_class = EmptySerializer is_job_start = True deprecated = True def get(self, request, *args, **kwargs): obj = self.get_object() data = dict( can_start=obj.can_start, ) if obj.can_start: data['passwords_needed_to_start'] = obj.passwords_needed_to_start data['ask_variables_on_launch'] = obj.ask_variables_on_launch return Response(data) def post(self, request, *args, **kwargs): obj = self.get_object() if obj.can_start: result = obj.signal_start(**request.data) if not result: data = dict(passwords_needed_to_start=obj.passwords_needed_to_start) return Response(data, status=status.HTTP_400_BAD_REQUEST) else: return Response(status=status.HTTP_202_ACCEPTED) else: return self.http_method_not_allowed(request, *args, **kwargs) class JobCancel(RetrieveAPIView): model = Job serializer_class = JobCancelSerializer is_job_cancel = True 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, GenericAPIView): model = Job serializer_class = JobRelaunchSerializer is_job_start = True @csrf_exempt @transaction.non_atomic_requests def dispatch(self, *args, **kwargs): return super(JobRelaunch, self).dispatch(*args, **kwargs) def post(self, request, *args, **kwargs): obj = self.get_object() # 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=request.data, context={'obj': obj, 'data': request.data}) if not serializer.is_valid(): return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) new_job = obj.copy_unified_job() result = new_job.signal_start(**request.data) if not result: data = dict(passwords_needed_to_start=new_job.passwords_needed_to_start) return Response(data, status=status.HTTP_400_BAD_REQUEST) else: data = JobSerializer(new_job, context=self.get_serializer_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 JobNotificationsList(SubListAPIView): model = Notification serializer_class = NotificationSerializer parent_model = Job relationship = 'notifications' new_in_300 = True class BaseJobHostSummariesList(SubListAPIView): model = JobHostSummary serializer_class = JobHostSummarySerializer parent_model = None # Subclasses must define this attribute. relationship = 'job_host_summaries' view_name = _('Job Host Summaries List') 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 = Host class GroupJobHostSummariesList(BaseJobHostSummariesList): parent_model = Group class JobJobHostSummariesList(BaseJobHostSummariesList): parent_model = Job class JobHostSummaryDetail(RetrieveAPIView): model = JobHostSummary serializer_class = JobHostSummarySerializer class JobEventList(ListAPIView): model = JobEvent serializer_class = JobEventSerializer class JobEventDetail(RetrieveAPIView): model = JobEvent serializer_class = JobEventSerializer class JobEventChildrenList(SubListAPIView): model = JobEvent serializer_class = JobEventSerializer parent_model = JobEvent relationship = 'children' view_name = _('Job Event Children List') class JobEventHostsList(SubListAPIView): model = Host serializer_class = HostSerializer parent_model = JobEvent relationship = 'hosts' view_name = _('Job Event Hosts List') class BaseJobEventsList(SubListAPIView): model = JobEvent serializer_class = JobEventSerializer parent_model = None # Subclasses must define this attribute. relationship = 'job_events' view_name = _('Job Events List') search_fields = ('stdout',) def finalize_response(self, request, response, *args, **kwargs): response['X-UI-Max-Events'] = settings.RECOMMENDED_MAX_EVENTS_DISPLAY_HEADER return super(BaseJobEventsList, self).finalize_response(request, response, *args, **kwargs) class HostJobEventsList(BaseJobEventsList): parent_model = 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( Q(host=parent_obj) | Q(hosts=parent_obj)).distinct() return qs class GroupJobEventsList(BaseJobEventsList): parent_model = Group class JobJobEventsList(BaseJobEventsList): parent_model = Job def get_queryset(self): job = self.get_parent_object() self.check_parent_access(job) qs = job.job_events qs = qs.select_related('host') qs = qs.prefetch_related('hosts', 'children') return qs.all() class AdHocCommandList(ListCreateAPIView): model = AdHocCommand serializer_class = AdHocCommandListSerializer new_in_220 = True always_allow_superuser = False @csrf_exempt @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 (Host, 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, (Host, 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(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) return Response(data, status=status.HTTP_400_BAD_REQUEST) return response class InventoryAdHocCommandsList(AdHocCommandList, SubListCreateAPIView): parent_model = Inventory relationship = 'ad_hoc_commands' parent_key = 'inventory' class GroupAdHocCommandsList(AdHocCommandList, SubListCreateAPIView): parent_model = Group relationship = 'ad_hoc_commands' class HostAdHocCommandsList(AdHocCommandList, SubListCreateAPIView): parent_model = Host relationship = 'ad_hoc_commands' class AdHocCommandDetail(RetrieveDestroyAPIView): model = AdHocCommand serializer_class = AdHocCommandSerializer new_in_220 = True class AdHocCommandCancel(RetrieveAPIView): model = AdHocCommand serializer_class = AdHocCommandCancelSerializer is_job_cancel = True new_in_220 = True 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 = AdHocCommand serializer_class = AdHocCommandRelaunchSerializer is_job_start = True new_in_220 = True # FIXME: Figure out why OPTIONS request still shows all fields. @csrf_exempt @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 = 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) return Response(data, status=status.HTTP_400_BAD_REQUEST) else: data = 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(ListAPIView): model = AdHocCommandEvent serializer_class = AdHocCommandEventSerializer new_in_220 = True class AdHocCommandEventDetail(RetrieveAPIView): model = AdHocCommandEvent serializer_class = AdHocCommandEventSerializer new_in_220 = True class BaseAdHocCommandEventsList(SubListAPIView): model = AdHocCommandEvent serializer_class = AdHocCommandEventSerializer parent_model = None # Subclasses must define this attribute. relationship = 'ad_hoc_command_events' view_name = _('Ad Hoc Command Events List') new_in_220 = True class HostAdHocCommandEventsList(BaseAdHocCommandEventsList): parent_model = Host new_in_220 = True #class GroupJobEventsList(BaseJobEventsList): # parent_model = Group class AdHocCommandAdHocCommandEventsList(BaseAdHocCommandEventsList): parent_model = AdHocCommand new_in_220 = True class AdHocCommandActivityStreamList(ActivityStreamEnforcementMixin, SubListAPIView): model = ActivityStream serializer_class = ActivityStreamSerializer parent_model = AdHocCommand relationship = 'activitystream_set' new_in_220 = True class AdHocCommandNotificationsList(SubListAPIView): model = Notification serializer_class = NotificationSerializer parent_model = AdHocCommand relationship = 'notifications' new_in_300 = True class SystemJobList(ListCreateAPIView): model = SystemJob serializer_class = SystemJobListSerializer new_in_210 = True 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(RetrieveDestroyAPIView): model = SystemJob serializer_class = SystemJobSerializer new_in_210 = True class SystemJobCancel(RetrieveAPIView): model = SystemJob serializer_class = SystemJobCancelSerializer is_job_cancel = True new_in_210 = True 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 = Notification serializer_class = NotificationSerializer parent_model = SystemJob relationship = 'notifications' new_in_300 = True class UnifiedJobTemplateList(ListAPIView): model = UnifiedJobTemplate serializer_class = UnifiedJobTemplateSerializer new_in_148 = True capabilities_prefetch = [ 'admin', 'execute', {'copy': ['jobtemplate.project.use', 'jobtemplate.inventory.use', 'jobtemplate.credential.use', 'jobtemplate.cloud_credential.use', 'jobtemplate.network_credential.use', 'workflowjobtemplate.organization.admin']} ] class UnifiedJobList(ListAPIView): model = UnifiedJob serializer_class = UnifiedJobListSerializer new_in_148 = True class StdoutANSIFilter(object): def __init__(self, fileobj): 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 # 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. line = re.sub(r'\x1b[^m]*m', '', line) data += line if size > 0 and len(data) > size: self.extra_data = data[size:] data = data[:size] else: self.extra_data = '' return data class UnifiedJobStdout(RetrieveAPIView): authentication_classes = [TokenGetAuthentication] + api_settings.DEFAULT_AUTHENTICATION_CLASSES serializer_class = UnifiedJobStdoutSerializer renderer_classes = [BrowsableAPIRenderer, renderers.StaticHTMLRenderer, PlainTextRenderer, AnsiTextRenderer, renderers.JSONRenderer, DownloadTextRenderer, AnsiDownloadRenderer] filter_backends = () new_in_148 = True def retrieve(self, request, *args, **kwargs): unified_job = self.get_object() obj_size = unified_job.result_stdout_size if request.accepted_renderer.format not in {'txt_download', 'ansi_download'} and obj_size > settings.STDOUT_MAX_BYTES_DISPLAY: response_message = _("Standard Output too large to display (%(text_size)d bytes), " "only download supported for sizes over %(supported_size)d bytes") % { 'text_size': obj_size, 'supported_size': settings.STDOUT_MAX_BYTES_DISPLAY} if request.accepted_renderer.format == 'json': return Response({'range': {'start': 0, 'end': 1, 'absolute_end': 1}, 'content': response_message}) else: return Response(response_message) if request.accepted_renderer.format in ('html', 'api', 'json'): content_format = request.query_params.get('content_format', 'html') 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(request.accepted_renderer.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(cgi.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 request.accepted_renderer.format == 'api': return Response(mark_safe(data)) if request.accepted_renderer.format == 'json': if content_encoding == 'base64' and content_format == 'ansi': return Response({'range': {'start': start, 'end': end, 'absolute_end': absolute_end}, 'content': b64encode(content)}) elif content_format == 'html': return Response({'range': {'start': start, 'end': end, 'absolute_end': absolute_end}, 'content': body}) return Response(data) elif request.accepted_renderer.format == 'txt': return Response(unified_job.result_stdout) elif request.accepted_renderer.format == 'ansi': return Response(unified_job.result_stdout_raw) elif request.accepted_renderer.format in {'txt_download', 'ansi_download'}: if not os.path.exists(unified_job.result_stdout_file): write_fd = open(unified_job.result_stdout_file, 'w') with connection.cursor() as cursor: try: cursor.copy_expert("copy (select stdout from main_jobevent where job_id={} order by start_line) to stdout".format(unified_job.id), write_fd) write_fd.close() subprocess.Popen("sed -i 's/\\\\r\\\\n/\\n/g' {}".format(unified_job.result_stdout_file), shell=True).wait() except Exception as e: return Response({"error": _("Error generating stdout download file: {}".format(e))}) try: content_fd = open(unified_job.result_stdout_file, 'r') if request.accepted_renderer.format == 'txt_download': # For txt downloads, filter out ANSI escape sequences. content_fd = StdoutANSIFilter(content_fd) suffix = '' else: suffix = '_ansi' response = HttpResponse(FileWrapper(content_fd), content_type='text/plain') response["Content-Disposition"] = 'attachment; filename="job_%s%s.txt"' % (str(unified_job.id), suffix) return response except Exception as e: return Response({"error": _("Error generating stdout download file: %s") % str(e)}, status=status.HTTP_400_BAD_REQUEST) else: return super(UnifiedJobStdout, self).retrieve(request, *args, **kwargs) class ProjectUpdateStdout(UnifiedJobStdout): model = ProjectUpdate new_in_13 = True class InventoryUpdateStdout(UnifiedJobStdout): model = InventoryUpdate class JobStdout(UnifiedJobStdout): model = Job class AdHocCommandStdout(UnifiedJobStdout): model = AdHocCommand new_in_220 = True class NotificationTemplateList(ListCreateAPIView): model = NotificationTemplate serializer_class = NotificationTemplateSerializer new_in_300 = True class NotificationTemplateDetail(RetrieveUpdateDestroyAPIView): model = NotificationTemplate serializer_class = NotificationTemplateSerializer new_in_300 = True 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) if obj.notifications.filter(status='pending').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): view_name = _('Notification Template Test') model = NotificationTemplate serializer_class = EmptySerializer new_in_300 = True is_job_start = True def post(self, request, *args, **kwargs): obj = self.get_object() notification = obj.generate_notification("Tower Notification Test {} {}".format(obj.id, settings.TOWER_URL_BASE), {"body": "Ansible Tower Test Notification {} {}".format(obj.id, settings.TOWER_URL_BASE)}) if not notification: return Response({}, status=status.HTTP_400_BAD_REQUEST) else: send_notifications.delay([notification.id]) headers = {'Location': notification.get_absolute_url(request=request)} return Response({"notification": notification.id}, headers=headers, status=status.HTTP_202_ACCEPTED) class NotificationTemplateNotificationList(SubListAPIView): model = Notification serializer_class = NotificationSerializer parent_model = NotificationTemplate relationship = 'notifications' parent_key = 'notification_template' new_in_300 = True class NotificationList(ListAPIView): model = Notification serializer_class = NotificationSerializer new_in_300 = True class NotificationDetail(RetrieveAPIView): model = Notification serializer_class = NotificationSerializer new_in_300 = True class LabelList(ListCreateAPIView): model = Label serializer_class = LabelSerializer new_in_300 = True class LabelDetail(RetrieveUpdateAPIView): model = Label serializer_class = LabelSerializer new_in_300 = True class ActivityStreamList(ActivityStreamEnforcementMixin, SimpleListAPIView): model = ActivityStream serializer_class = ActivityStreamSerializer new_in_145 = True class ActivityStreamDetail(ActivityStreamEnforcementMixin, RetrieveAPIView): model = ActivityStream serializer_class = ActivityStreamSerializer new_in_145 = True class RoleList(ListAPIView): model = Role serializer_class = RoleSerializer permission_classes = (IsAuthenticated,) new_in_300 = True def get_queryset(self): result = Role.visible_roles(self.request.user) # Sanity check: is the requesting user an orphaned non-admin/auditor? # if yes, make system admin/auditor mandatorily visible. if not self.request.user.organizations.exists() and\ not self.request.user.is_superuser and\ not self.request.user.is_system_auditor: mandatories = ('system_administrator', 'system_auditor') super_qs = Role.objects.filter(singleton_name__in=mandatories) result = result | super_qs return result class RoleDetail(RetrieveAPIView): model = Role serializer_class = RoleSerializer new_in_300 = True class RoleUsersList(SubListCreateAttachDetachAPIView): model = User serializer_class = UserSerializer parent_model = Role relationship = 'members' new_in_300 = True 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: data = dict(msg=_("User 'id' field is missing.")) return Response(data, status=status.HTTP_400_BAD_REQUEST) user = get_object_or_400(User, pk=sub_id) role = self.get_parent_object() if role == self.request.user.admin_role: raise PermissionDenied(_('You may not perform any action with your own admin_role.')) user_content_type = ContentType.objects.get_for_model(User) if role.content_type == user_content_type: raise PermissionDenied(_('You may not change the membership of a users admin_role')) credential_content_type = ContentType.objects.get_for_model(Credential) if role.content_type == credential_content_type: if 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(SubListAPIView): model = Team serializer_class = TeamSerializer parent_model = Role relationship = 'member_role.parents' permission_classes = (IsAuthenticated,) new_in_300 = True def get_queryset(self): role = self.get_parent_object() self.check_parent_access(role) return Team.objects.filter(member_role__children=role) def post(self, request, pk, *args, **kwargs): # Forbid implicit team creation here sub_id = request.data.get('id', None) if not sub_id: data = dict(msg=_("Team 'id' field is missing.")) return Response(data, status=status.HTTP_400_BAD_REQUEST) team = get_object_or_400(Team, pk=sub_id) role = Role.objects.get(pk=self.kwargs['pk']) organization_content_type = ContentType.objects.get_for_model(Organization) if role.content_type == organization_content_type: data = dict(msg=_("You cannot assign an Organization 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(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 = Role serializer_class = RoleSerializer parent_model = Role relationship = 'parents' permission_classes = (IsAuthenticated,) new_in_300 = True def get_queryset(self): role = Role.objects.get(pk=self.kwargs['pk']) return Role.filter_visible_roles(self.request.user, role.parents.all()) class RoleChildrenList(SubListAPIView): model = Role serializer_class = RoleSerializer parent_model = Role relationship = 'children' permission_classes = (IsAuthenticated,) new_in_300 = True def get_queryset(self): role = Role.objects.get(pk=self.kwargs['pk']) return 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 locals().items(): if isinstance(value, type) and issubclass(value, APIView): name = camelcase_to_underscore(attr) view = value.as_view() setattr(this_module, name, view)