adjutant/adjutant/api/v1/utils.py

258 lines
7.6 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.
import hashlib
import json
from datetime import timedelta
from smtplib import SMTPException
from uuid import uuid4
from decorator import decorator
from django.conf import settings
from django.core.exceptions import FieldError
from django.core.mail import EmailMultiAlternatives
from django.template import loader
from django.utils import timezone
from rest_framework.response import Response
from adjutant.api.models import Notification, Token
def create_token(task):
expire = timezone.now() + timedelta(hours=settings.TOKEN_EXPIRE_TIME)
uuid = uuid4().hex
token = Token.objects.create(
task=task,
token=uuid,
expires=expire
)
token.save()
return token
def send_stage_email(task, email_conf, token=None):
if not email_conf:
return
text_template = loader.get_template(
email_conf['template'],
using='include_etc_templates')
html_template = email_conf.get('html_template', None)
if html_template:
html_template = loader.get_template(
html_template,
using='include_etc_templates')
emails = set()
actions = {}
# find our set of emails and actions that require email
for action in task.actions:
act = action.get_action()
email = act.get_email()
if email:
emails.add(email)
actions[str(act)] = act
if not emails:
return
if len(emails) > 1:
notes = {
'errors':
("Error: Unable to send update, more than one email for task: %s"
% task.uuid)
}
create_notification(task, notes, error=True)
return
context = {
'task': task,
'actions': actions
}
if token:
if settings.HORIZON_URL:
tokenurl = settings.HORIZON_URL
if not tokenurl.endswith('/'):
tokenurl += '/'
tokenurl += 'token/'
else:
tokenurl = settings.TOKEN_SUBMISSION_URL
if not tokenurl.endswith('/'):
tokenurl += '/'
context.update({
'tokenurl': tokenurl,
'token': token.token
})
try:
message = text_template.render(context)
# from_email is the return-path and is distinct from the
# message headers
from_email = email_conf.get('from')
if not from_email:
from_email = email_conf['reply']
elif "%(task_uuid)s" in from_email:
from_email = from_email % {'task_uuid': task.uuid}
# these are the message headers which will be visible to
# the email client.
headers = {
'X-Adjutant-Task-UUID': task.uuid,
# From needs to be set to be disctinct from return-path
'From': email_conf['reply'],
'Reply-To': email_conf['reply'],
}
email = EmailMultiAlternatives(
email_conf['subject'],
message,
from_email,
[emails.pop()],
headers=headers,
)
if html_template:
email.attach_alternative(
html_template.render(context), "text/html")
email.send(fail_silently=False)
except SMTPException as e:
notes = {
'errors':
("Error: '%s' while emailing update for task: %s" %
(e, task.uuid))
}
errors_conf = settings.TASK_SETTINGS.get(
task.task_type, settings.DEFAULT_TASK_SETTINGS).get(
'errors', {}).get("SMTPException", {})
if errors_conf:
notification = create_notification(
task, notes, error=True,
engines=errors_conf.get('engines', True))
if errors_conf.get('notification') == "acknowledge":
notification.acknowledged = True
notification.save()
else:
create_notification(task, notes, error=True)
def create_notification(task, notes, error=False, engines=True):
notification = Notification.objects.create(
task=task,
notes=notes,
error=error
)
notification.save()
if not engines:
return notification
class_conf = settings.TASK_SETTINGS.get(
task.task_type, settings.DEFAULT_TASK_SETTINGS)
notification_conf = class_conf.get('notifications', {})
if notification_conf:
for note_engine, conf in notification_conf.items():
if error:
conf = conf.get('error', {})
else:
conf = conf.get('standard', {})
if not conf:
continue
engine = settings.NOTIFICATION_ENGINES[note_engine](conf)
engine.notify(task, notification)
return notification
def create_task_hash(task_type, action_list):
hashable_list = [task_type, ]
for action in action_list:
hashable_list.append(action['name'])
if not action['serializer']:
continue
# iterate like this to maintain consistent order for hash
fields = sorted(action['serializer'].validated_data.keys())
for field in fields:
try:
hashable_list.append(
action['serializer'].validated_data[field])
except KeyError:
if field is "username" and settings.USERNAME_IS_EMAIL:
continue
else:
raise
return hashlib.sha256(str(hashable_list).encode('utf-8')).hexdigest()
# "{'filters': {'fieldname': { 'operation': 'value'}}
@decorator
def parse_filters(func, *args, **kwargs):
"""
Parses incoming filters paramters and converts them to
Django usable operations if valid.
BE AWARE! WILL NOT WORK UNLESS POSITIONAL ARGUMENT 3 IS FILTERS!
"""
request = args[1]
filters = request.query_params.get('filters', None)
if not filters:
return func(*args, **kwargs)
cleaned_filters = {}
try:
filters = json.loads(filters)
for field, operations in filters.items():
for operation, value in operations.items():
cleaned_filters['%s__%s' % (field, operation)] = value
except (ValueError, AttributeError):
return Response(
{'errors': [
"Filters incorrectly formatted. Required format: "
"{'filters': {'fieldname': { 'operation': 'value'}}"
]},
status=400
)
try:
# NOTE(adriant): This feels dirty and unclear, but it works.
# Positional argument 3 is filters, so we just replace it.
args = list(args)
args[2] = cleaned_filters
return func(*args, **kwargs)
except FieldError as e:
return Response({'errors': [str(e)]}, status=400)
def add_task_id_for_roles(request, processed, response_dict, req_roles):
if request.keystone_user.get('authenticated', False):
req_roles = set(req_roles)
roles = set(request.keystone_user.get('roles', []))
if roles & req_roles:
response_dict['task'] = processed['task'].uuid