adjutant/adjutant/api/v1/views.py

799 lines
27 KiB
Python

# Copyright (C) 2015 Catalyst IT Ltd
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from logging import getLogger
from django.conf import settings
from django.utils import timezone
from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
from rest_framework.exceptions import ParseError
from rest_framework.response import Response
from rest_framework.views import APIView
from adjutant.api import utils
from adjutant.api.models import Notification, Task, Token
from adjutant.api.v1.utils import (
create_notification, create_token, parse_filters, send_stage_email)
class APIViewWithLogger(APIView):
"""
APIView with a logger.
"""
def __init__(self, *args, **kwargs):
super(APIViewWithLogger, self).__init__(*args, **kwargs)
self.logger = getLogger('adjutant')
class StatusView(APIViewWithLogger):
@utils.admin
def get(self, request, filters=None, format=None):
"""
Simple status endpoint.
Returns a list of unacknowledged error notifications,
and both the last created and last completed tasks.
Can returns None, if there are no tasks.
"""
notifications = Notification.objects.filter(
error=1,
acknowledged=0
)
try:
last_created_task = Task.objects.filter(
completed=0).order_by("-created_on")[0].to_dict()
except IndexError:
last_created_task = None
try:
last_completed_task = Task.objects.filter(
completed=1).order_by("-completed_on")[0].to_dict()
except IndexError:
last_completed_task = None
status = {
"error_notifications": [note.to_dict() for note in notifications],
"last_created_task": last_created_task,
"last_completed_task": last_completed_task
}
return Response(status, status=200)
class NotificationList(APIViewWithLogger):
@utils.admin
@parse_filters
def get(self, request, filters=None, format=None):
"""
A list of Notification objects as dicts.
"""
if filters:
notifications = Notification.objects.filter(
**filters).order_by("-created_on")
else:
notifications = Notification.objects.all().order_by("-created_on")
note_list = []
for notification in notifications:
note_list.append(notification.to_dict())
return Response({"notifications": note_list}, status=200)
@utils.admin
def post(self, request, format=None):
"""
Acknowledge notifications.
"""
note_list = request.data.get('notifications', None)
if note_list and isinstance(note_list, list):
notifications = Notification.objects.filter(uuid__in=note_list)
for notification in notifications:
notification.acknowledged = True
notification.save()
return Response({'notes': ['Notifications acknowledged.']},
status=200)
else:
return Response(
{'notifications': [
"this field is required and needs to be a list."
]},
status=400)
class NotificationDetail(APIViewWithLogger):
@utils.admin
def get(self, request, uuid, format=None):
"""
Dict notification of a Notification object
and its related actions.
"""
try:
notification = Notification.objects.get(uuid=uuid)
except Notification.DoesNotExist:
return Response(
{'errors': ['No notification with this id.']},
status=404)
return Response(notification.to_dict())
@utils.admin
def post(self, request, uuid, format=None):
"""
Acknowledge notification.
"""
try:
notification = Notification.objects.get(uuid=uuid)
except Notification.DoesNotExist:
return Response(
{'errors': ['No notification with this id.']},
status=404)
if notification.acknowledged:
return Response({'notes': ['Notification already acknowledged.']},
status=200)
if request.data.get('acknowledged', False) is True:
notification.acknowledged = True
notification.save()
return Response({'notes': ['Notification acknowledged.']},
status=200)
else:
return Response({'acknowledged': ["this field is required."]},
status=400)
class TaskList(APIViewWithLogger):
@utils.admin
@parse_filters
def get(self, request, filters=None, format=None):
"""
A list of dict representations of Task objects
and their related actions.
"""
page = request.GET.get('page', 1)
tasks_per_page = request.GET.get('tasks_per_page', None)
if 'admin' in request.keystone_user['roles']:
if filters:
tasks = Task.objects.filter(**filters).order_by("-created_on")
else:
tasks = Task.objects.all().order_by("-created_on")
if tasks_per_page:
paginator = Paginator(tasks, tasks_per_page)
try:
tasks = paginator.page(page)
except EmptyPage:
return Response({'tasks': [],
'pages': paginator.num_pages,
'has_more': False,
'has_prev': False}, status=200)
# NOTE(amelia): 'has_more'and 'has_prev' names are
# based on the horizon pagination table pagination names
except PageNotAnInteger:
return Response({'error': 'Page not an integer'},
status=400)
task_list = []
for task in tasks:
task_list.append(task._to_dict())
if tasks_per_page:
return Response({'tasks': task_list,
'pages': paginator.num_pages,
'has_more': tasks.has_next(),
'has_prev': tasks.has_previous()}, status=200)
else:
return Response({'tasks': task_list})
else:
if filters:
# Ignore any filters with project_id in them
for field_filter in filters.keys():
if "project_id" in field_filter:
filters.pop(field_filter)
tasks = Task.objects.filter(
project_id__exact=request.keystone_user['project_id'],
**filters).order_by("-created_on")
else:
tasks = Task.objects.filter(
project_id__exact=request.keystone_user['project_id']
).order_by("-created_on")
paginator = Paginator(tasks, tasks_per_page)
tasks = paginator.page(page)
task_list = []
for task in tasks:
task_list.append(task.to_dict())
return Response({'tasks': task_list,
'pages': paginator.num_pages}, status=200)
class TaskDetail(APIViewWithLogger):
@utils.mod_or_admin
def get(self, request, uuid, format=None):
"""
Dict representation of a Task object
and its related actions.
"""
try:
if 'admin' in request.keystone_user['roles']:
task = Task.objects.get(uuid=uuid)
return Response(task._to_dict())
else:
task = Task.objects.get(
uuid=uuid, project_id=request.keystone_user['project_id'])
return Response(task.to_dict())
except Task.DoesNotExist:
return Response(
{'errors': ['No task with this id.']},
status=404)
@utils.admin
def put(self, request, uuid, format=None):
"""
Allows the updating of action data and retriggering
of the pre_approve step.
"""
try:
task = Task.objects.get(uuid=uuid)
except Task.DoesNotExist:
return Response(
{'errors': ['No task with this id.']},
status=404)
if task.completed:
return Response(
{'errors':
['This task has already been completed.']},
status=400)
if task.cancelled:
# NOTE(adriant): If we can uncancel a task, that should happen
# at this endpoint.
return Response(
{'errors':
['This task has been cancelled.']},
status=400)
if task.approved:
return Response(
{'errors':
['This task has already been approved.']},
status=400)
act_list = []
valid = True
for action in task.actions:
action_serializer = settings.ACTION_CLASSES[action.action_name][1]
if action_serializer is not None:
serializer = action_serializer(data=request.data)
else:
serializer = None
act_list.append({
'name': action.action_name,
'action': action,
'serializer': serializer})
if serializer is not None and not serializer.is_valid():
valid = False
if valid:
for act in act_list:
if act['serializer'] is not None:
data = act['serializer'].validated_data
else:
data = {}
act['action'].action_data = data
act['action'].save()
try:
act['action'].get_action().pre_approve()
except Exception as e:
notes = {
'errors':
[("Error: '%s' while updating task. " +
"See task itself for details.") % e],
'task': task.uuid
}
create_notification(task, notes, error=True)
import traceback
trace = traceback.format_exc()
self.logger.critical(("(%s) - Exception escaped! %s\n" +
"Trace: \n%s") %
(timezone.now(), e, trace))
response_dict = {
'errors':
["Error: Something went wrong on the server. " +
"It will be looked into shortly."]
}
return Response(response_dict, status=500)
return Response(
{'notes': ["Task successfully updated."]},
status=200)
else:
errors = {}
for act in act_list:
if act['serializer'] is not None:
errors.update(act['serializer'].errors)
return Response({'errors': errors}, status=400)
@utils.admin
def post(self, request, uuid, format=None):
"""
Will approve the Task specified,
followed by running the post_approve actions
and if valid will setup and create a related token.
"""
try:
task = Task.objects.get(uuid=uuid)
except Task.DoesNotExist:
return Response(
{'errors': ['No task with this id.']},
status=404)
try:
if request.data.get('approved') is not True:
return Response(
{'approved': ["this is a required boolean field."]},
status=400)
except ParseError:
return Response(
{'approved': ["this is a required boolean field."]},
status=400)
if task.completed:
return Response(
{'errors':
['This task has already been completed.']},
status=400)
if task.cancelled:
return Response(
{'errors':
['This task has been cancelled.']},
status=400)
# we check that the task is valid before approving it:
valid = True
for action in task.actions:
if not action.valid:
valid = False
if not valid:
return Response(
{'errors':
['Cannot approve an invalid task. ' +
'Update data and rerun pre_approve.']},
status=400)
if task.approved:
# Expire previously in use tokens
Token.objects.filter(task=task.uuid).delete()
# We approve the task before running actions,
# that way if something goes wrong we know if it was approved,
# when it was approved, and who approved it last. Subsequent
# reapproval attempts overwrite previous approved_by/on.
task.approved = True
task.approved_by = request.keystone_user
task.approved_on = timezone.now()
task.save()
need_token = False
valid = True
actions = []
for action in task.actions:
act_model = action.get_action()
actions.append(act_model)
try:
act_model.post_approve()
except Exception as e:
notes = {
'errors':
[("Error: '%s' while approving task. " +
"See task itself for details.") % e],
'task': task.uuid
}
create_notification(task, notes, error=True)
import traceback
trace = traceback.format_exc()
self.logger.critical(("(%s) - Exception escaped! %s\n" +
"Trace: \n%s") %
(timezone.now(), e, trace))
return Response(notes, status=500)
if not action.valid:
valid = False
if action.need_token:
need_token = True
if valid:
if need_token:
token = create_token(task)
try:
class_conf = settings.TASK_SETTINGS.get(
task.task_type, settings.DEFAULT_TASK_SETTINGS)
# will throw a key error if the token template has not
# been specified
email_conf = class_conf['emails']['token']
send_stage_email(task, email_conf, token)
return Response({'notes': ['created token']},
status=200)
except KeyError as e:
notes = {
'errors':
[("Error: '%s' while sending " +
"token. See task " +
"itself for details.") % e],
'task': task.uuid
}
create_notification(task, notes, error=True)
import traceback
trace = traceback.format_exc()
self.logger.critical(("(%s) - Exception escaped!" +
" %s\n Trace: \n%s") %
(timezone.now(), e, trace))
response_dict = {
'errors':
["Error: Something went wrong on the " +
"server. It will be looked into shortly."]
}
return Response(response_dict, status=500)
else:
for action in actions:
try:
action.submit({})
except Exception as e:
notes = {
'errors':
[("Error: '%s' while submitting " +
"task. See task " +
"itself for details.") % e],
'task': task.uuid
}
create_notification(task, notes, error=True)
import traceback
trace = traceback.format_exc()
self.logger.critical(("(%s) - Exception escaped!" +
" %s\n Trace: \n%s") %
(timezone.now(), e, trace))
return Response(notes, status=500)
task.completed = True
task.completed_on = timezone.now()
task.save()
# Sending confirmation email:
class_conf = settings.TASK_SETTINGS.get(
task.task_type, settings.DEFAULT_TASK_SETTINGS)
email_conf = class_conf.get(
'emails', {}).get('completed', None)
send_stage_email(task, email_conf)
return Response(
{'notes': ["Task completed successfully."]},
status=200)
return Response({'errors': ['actions invalid']}, status=400)
@utils.mod_or_admin
def delete(self, request, uuid, format=None):
"""
Cancel the Task.
Project Admins and Project Mods can only cancel tasks
associated with their project.
"""
try:
if 'admin' in request.keystone_user['roles']:
task = Task.objects.get(uuid=uuid)
else:
task = Task.objects.get(
uuid=uuid, project_id=request.keystone_user['project_id'])
except Task.DoesNotExist:
return Response(
{'errors': ['No task with this id.']},
status=404)
if task.completed:
return Response(
{'errors':
['This task has already been completed.']},
status=400)
if task.cancelled:
return Response(
{'errors':
['This task has already been cancelled.']},
status=400)
task.cancelled = True
task.save()
return Response(
{'notes': ["Task cancelled successfully."]},
status=200)
class TokenList(APIViewWithLogger):
"""
Admin functionality for managing/monitoring tokens.
"""
@utils.admin
@parse_filters
def get(self, request, filters=None, format=None):
"""
A list of dict representations of Token objects.
"""
if filters:
tokens = Token.objects.filter(**filters).order_by("-created_on")
else:
tokens = Token.objects.all().order_by("-created_on")
token_list = []
for token in tokens:
token_list.append(token.to_dict())
return Response({"tokens": token_list})
@utils.mod_or_admin
def post(self, request, format=None):
"""
Reissue a token for an approved task.
Clears other tokens for it.
"""
uuid = request.data.get('task', None)
if uuid is None:
return Response(
{'task': ["This field is required.", ]},
status=400)
try:
if 'admin' in request.keystone_user['roles']:
task = Task.objects.get(uuid=uuid)
else:
task = Task.objects.get(
uuid=uuid, project_id=request.keystone_user['project_id'])
except Task.DoesNotExist:
return Response(
{'errors': ['No task with this id.']},
status=404)
if task.completed:
return Response(
{'errors':
['This task has already been completed.']},
status=400)
if task.cancelled:
return Response(
{'errors':
['This task has been cancelled.']},
status=400)
if not task.approved:
return Response(
{'errors': ['This task has not been approved.']},
status=400)
for token in task.tokens:
token.delete()
token = create_token(task)
try:
class_conf = settings.TASK_SETTINGS.get(
task.task_type, settings.DEFAULT_TASK_SETTINGS)
# will throw a key error if the token template has not
# been specified
email_conf = class_conf['emails']['token']
send_stage_email(task, email_conf, token)
except KeyError as e:
notes = {
'errors': [
("Error: '%(error)s' while sending token. " +
"See registration itself for details.") % {'error': e}
],
'task': task.uuid
}
create_notification(task, notes, error=True)
import traceback
trace = traceback.format_exc()
self.logger.critical(("(%s) - Exception escaped!" +
" %s\n Trace: \n%s") %
(timezone.now(), e, trace))
response_dict = {
'errors':
["Error: Something went wrong on the " +
"server. It will be looked into shortly."]
}
return Response(response_dict, status=500)
return Response(
{'notes': ['Token reissued.']}, status=200)
@utils.admin
def delete(self, request, format=None):
"""
Delete all expired tokens.
"""
now = timezone.now()
Token.objects.filter(expires__lt=now).delete()
return Response(
{'notes': ['Deleted all expired tokens.']}, status=200)
class TokenDetail(APIViewWithLogger):
def get(self, request, id, format=None):
"""
Returns a response with the list of required fields
and what actions those go towards.
"""
try:
token = Token.objects.get(token=id)
if token.expires < timezone.now():
token.delete()
token = Token.objects.get(token=id)
except Token.DoesNotExist:
return Response(
{'errors': ['This token does not exist or has expired.']},
status=404)
if token.task.completed:
return Response(
{'errors':
['This task has already been completed.']},
status=400)
if token.task.cancelled:
return Response(
{'errors':
['This task has been cancelled.']},
status=400)
required_fields = []
actions = []
for action in token.task.actions:
action = action.get_action()
actions.append(action)
for field in action.token_fields:
if field not in required_fields:
required_fields.append(field)
return Response({'actions': [str(act) for act in actions],
'required_fields': required_fields,
'task_type': token.task.task_type})
def post(self, request, id, format=None):
"""
Ensures the required fields are present,
will then pass those to the actions via the submit
function.
"""
try:
token = Token.objects.get(token=id)
if token.expires < timezone.now():
token.delete()
token = Token.objects.get(token=id)
except Token.DoesNotExist:
return Response(
{'errors': ['This token does not exist or has expired.']},
status=404)
if token.task.completed:
return Response(
{'errors':
['This task has already been completed.']},
status=400)
if token.task.cancelled:
return Response(
{'errors':
['This task has been cancelled.']},
status=400)
required_fields = set()
actions = []
for action in token.task.actions:
a = action.get_action()
actions.append(a)
for field in a.token_fields:
required_fields.add(field)
errors = {}
data = {}
for field in required_fields:
try:
data[field] = request.data[field]
except KeyError:
errors[field] = ["This field is required.", ]
except TypeError:
errors = ["Improperly formated json. " +
"Should be a key-value object.", ]
break
if errors:
return Response({"errors": errors}, status=400)
valid = True
for action in actions:
try:
action.submit(data)
if not action.valid:
valid = False
except Exception as e:
notes = {
'errors':
[("Error: '%s' while submitting task. " +
"See task itself for details.") % e],
'task': token.task.uuid
}
create_notification(token.task, notes, error=True)
import traceback
trace = traceback.format_exc()
self.logger.critical(("(%s) - Exception escaped! %s\n" +
"Trace: \n%s") %
(timezone.now(), e, trace))
response_dict = {
'errors':
["Error: Something went wrong on the server. " +
"It will be looked into shortly."]
}
return Response(response_dict, status=500)
token.task.completed = True
token.task.completed_on = timezone.now()
token.task.save()
token.delete()
if not valid:
return Response({"errors": ["Actions invalid"]}, status=400)
# Sending confirmation email:
class_conf = settings.TASK_SETTINGS.get(
token.task.task_type, settings.DEFAULT_TASK_SETTINGS)
email_conf = class_conf.get(
'emails', {}).get('completed', None)
send_stage_email(token.task, email_conf)
return Response(
{'notes': ["Token submitted successfully."]},
status=200)