2014-07-18 02:51:58 +00:00
|
|
|
import logging
|
|
|
|
import io
|
|
|
|
import os.path
|
|
|
|
import tarfile
|
|
|
|
import base64
|
|
|
|
import json
|
2014-08-19 18:33:33 +00:00
|
|
|
import requests
|
2014-08-27 02:09:56 +00:00
|
|
|
import re
|
2014-07-18 02:51:58 +00:00
|
|
|
|
|
|
|
from flask.ext.mail import Message
|
2014-08-19 18:33:33 +00:00
|
|
|
from app import mail, app, get_app_url
|
2014-07-18 02:51:58 +00:00
|
|
|
from data import model
|
|
|
|
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
|
|
class InvalidNotificationMethodException(Exception):
|
|
|
|
pass
|
|
|
|
|
2014-07-28 18:58:12 +00:00
|
|
|
class CannotValidateNotificationMethodException(Exception):
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
2014-07-18 02:51:58 +00:00
|
|
|
class NotificationMethod(object):
|
|
|
|
def __init__(self):
|
|
|
|
pass
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
def method_name(cls):
|
|
|
|
"""
|
|
|
|
Particular method implemented by subclasses.
|
|
|
|
"""
|
|
|
|
raise NotImplementedError
|
|
|
|
|
2014-07-28 18:58:12 +00:00
|
|
|
def validate(self, repository, config_data):
|
|
|
|
"""
|
|
|
|
Validates that the notification can be created with the given data. Throws
|
|
|
|
a CannotValidateNotificationMethodException on failure.
|
|
|
|
"""
|
|
|
|
raise NotImplementedError
|
|
|
|
|
2014-07-18 02:51:58 +00:00
|
|
|
def perform(self, notification, event_handler, notification_data):
|
|
|
|
"""
|
|
|
|
Performs the notification method.
|
|
|
|
|
|
|
|
notification: The noticication record itself.
|
|
|
|
event_handler: The NotificationEvent handler.
|
|
|
|
notification_data: The dict of notification data placed in the queue.
|
|
|
|
"""
|
|
|
|
raise NotImplementedError
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
def get_method(cls, methodname):
|
|
|
|
for subc in cls.__subclasses__():
|
|
|
|
if subc.method_name() == methodname:
|
|
|
|
return subc()
|
|
|
|
|
|
|
|
raise InvalidNotificationMethodException('Unable to find method: %s' % methodname)
|
|
|
|
|
|
|
|
|
|
|
|
class QuayNotificationMethod(NotificationMethod):
|
|
|
|
@classmethod
|
|
|
|
def method_name(cls):
|
|
|
|
return 'quay_notification'
|
|
|
|
|
2014-07-28 18:58:12 +00:00
|
|
|
def validate(self, repository, config_data):
|
|
|
|
status, err_message, target_users = self.find_targets(repository, config_data)
|
|
|
|
if err_message:
|
|
|
|
raise CannotValidateNotificationMethodException(err_message)
|
|
|
|
|
|
|
|
def find_targets(self, repository, config_data):
|
2014-07-18 18:12:20 +00:00
|
|
|
target_info = config_data['target']
|
|
|
|
|
|
|
|
if target_info['kind'] == 'user':
|
|
|
|
target = model.get_user(target_info['name'])
|
|
|
|
if not target:
|
|
|
|
# Just to be safe.
|
2014-07-28 18:58:12 +00:00
|
|
|
return (True, 'Unknown user %s' % target_info['name'], [])
|
2014-07-18 18:12:20 +00:00
|
|
|
|
2014-07-28 18:58:12 +00:00
|
|
|
return (True, None, [target])
|
2014-07-22 17:39:41 +00:00
|
|
|
elif target_info['kind'] == 'org':
|
|
|
|
target = model.get_organization(target_info['name'])
|
|
|
|
if not target:
|
|
|
|
# Just to be safe.
|
2014-07-28 18:58:12 +00:00
|
|
|
return (True, 'Unknown organization %s' % target_info['name'], None)
|
2014-07-22 17:39:41 +00:00
|
|
|
|
|
|
|
# Only repositories under the organization can cause notifications to that org.
|
|
|
|
if target_info['name'] != repository.namespace:
|
2014-07-28 18:58:12 +00:00
|
|
|
return (False, 'Organization name must match repository namespace')
|
2014-07-22 17:39:41 +00:00
|
|
|
|
2014-07-28 18:58:12 +00:00
|
|
|
return (True, None, [target])
|
2014-07-18 18:12:20 +00:00
|
|
|
elif target_info['kind'] == 'team':
|
|
|
|
# Lookup the team.
|
|
|
|
team = None
|
|
|
|
try:
|
|
|
|
team = model.get_organization_team(repository.namespace, target_info['name'])
|
|
|
|
except model.InvalidTeamException:
|
|
|
|
# Probably deleted.
|
2014-07-28 18:58:12 +00:00
|
|
|
return (True, 'Unknown team %s' % target_info['name'], None)
|
2014-07-18 18:12:20 +00:00
|
|
|
|
|
|
|
# Lookup the team's members
|
2014-07-28 18:58:12 +00:00
|
|
|
return (True, None, model.get_organization_team_members(team.id))
|
|
|
|
|
|
|
|
|
|
|
|
def perform(self, notification, event_handler, notification_data):
|
2014-07-31 17:30:54 +00:00
|
|
|
repository = notification.repository
|
2014-07-28 18:58:12 +00:00
|
|
|
if not repository:
|
|
|
|
# Probably deleted.
|
|
|
|
return True
|
|
|
|
|
|
|
|
# Lookup the target user or team to which we'll send the notification.
|
|
|
|
config_data = json.loads(notification.config_json)
|
|
|
|
status, err_message, target_users = self.find_targets(repository, config_data)
|
|
|
|
if not status:
|
|
|
|
return False
|
2014-07-18 18:12:20 +00:00
|
|
|
|
|
|
|
# For each of the target users, create a notification.
|
2014-07-28 18:58:12 +00:00
|
|
|
for target_user in set(target_users or []):
|
2014-07-18 18:12:20 +00:00
|
|
|
model.create_notification(event_handler.event_name(), target_user,
|
|
|
|
metadata=notification_data['event_data'])
|
2014-07-18 02:51:58 +00:00
|
|
|
return True
|
|
|
|
|
|
|
|
|
|
|
|
class EmailMethod(NotificationMethod):
|
|
|
|
@classmethod
|
|
|
|
def method_name(cls):
|
|
|
|
return 'email'
|
|
|
|
|
2014-07-28 18:58:12 +00:00
|
|
|
def validate(self, repository, config_data):
|
|
|
|
email = config_data.get('email', '')
|
|
|
|
if not email:
|
|
|
|
raise CannotValidateNotificationMethodException('Missing e-mail address')
|
|
|
|
|
|
|
|
record = model.get_email_authorized_for_repo(repository.namespace, repository.name, email)
|
|
|
|
if not record or not record.confirmed:
|
|
|
|
raise CannotValidateNotificationMethodException('The specified e-mail address '
|
|
|
|
'is not authorized to receive '
|
|
|
|
'notifications for this repository')
|
|
|
|
|
|
|
|
|
2014-07-18 02:51:58 +00:00
|
|
|
def perform(self, notification, event_handler, notification_data):
|
|
|
|
config_data = json.loads(notification.config_json)
|
|
|
|
email = config_data.get('email', '')
|
|
|
|
if not email:
|
|
|
|
return False
|
|
|
|
|
2014-07-18 19:58:18 +00:00
|
|
|
msg = Message(event_handler.get_summary(notification_data['event_data'], notification_data),
|
2014-07-18 02:51:58 +00:00
|
|
|
sender='support@quay.io',
|
|
|
|
recipients=[email])
|
2014-07-18 19:58:18 +00:00
|
|
|
msg.html = event_handler.get_message(notification_data['event_data'], notification_data)
|
2014-07-18 02:51:58 +00:00
|
|
|
|
|
|
|
try:
|
2014-07-18 15:52:36 +00:00
|
|
|
with app.app_context():
|
|
|
|
mail.send(msg)
|
2014-07-18 02:51:58 +00:00
|
|
|
except Exception as ex:
|
|
|
|
logger.exception('Email was unable to be sent: %s' % ex.message)
|
|
|
|
return False
|
|
|
|
|
|
|
|
return True
|
|
|
|
|
|
|
|
|
|
|
|
class WebhookMethod(NotificationMethod):
|
|
|
|
@classmethod
|
|
|
|
def method_name(cls):
|
|
|
|
return 'webhook'
|
|
|
|
|
2014-07-28 18:58:12 +00:00
|
|
|
def validate(self, repository, config_data):
|
|
|
|
url = config_data.get('url', '')
|
|
|
|
if not url:
|
|
|
|
raise CannotValidateNotificationMethodException('Missing webhook URL')
|
|
|
|
|
2014-07-18 02:51:58 +00:00
|
|
|
def perform(self, notification, event_handler, notification_data):
|
|
|
|
config_data = json.loads(notification.config_json)
|
|
|
|
url = config_data.get('url', '')
|
|
|
|
if not url:
|
|
|
|
return False
|
|
|
|
|
|
|
|
payload = notification_data['event_data']
|
|
|
|
headers = {'Content-type': 'application/json'}
|
|
|
|
|
|
|
|
try:
|
|
|
|
resp = requests.post(url, data=json.dumps(payload), headers=headers)
|
|
|
|
if resp.status_code/100 != 2:
|
|
|
|
logger.error('%s response for webhook to url: %s' % (resp.status_code,
|
|
|
|
url))
|
|
|
|
return False
|
|
|
|
|
|
|
|
except requests.exceptions.RequestException as ex:
|
|
|
|
logger.exception('Webhook was unable to be sent: %s' % ex.message)
|
|
|
|
return False
|
|
|
|
|
|
|
|
return True
|
2014-08-19 18:33:33 +00:00
|
|
|
|
|
|
|
|
|
|
|
class FlowdockMethod(NotificationMethod):
|
|
|
|
""" Method for sending notifications to Flowdock via the Team Inbox API:
|
|
|
|
https://www.flowdock.com/api/team-inbox
|
|
|
|
"""
|
|
|
|
@classmethod
|
|
|
|
def method_name(cls):
|
|
|
|
return 'flowdock'
|
|
|
|
|
|
|
|
def validate(self, repository, config_data):
|
|
|
|
token = config_data.get('flow_api_token', '')
|
|
|
|
if not token:
|
|
|
|
raise CannotValidateNotificationMethodException('Missing Flowdock API Token')
|
|
|
|
|
|
|
|
def perform(self, notification, event_handler, notification_data):
|
|
|
|
config_data = json.loads(notification.config_json)
|
|
|
|
token = config_data.get('flow_api_token', '')
|
|
|
|
if not token:
|
|
|
|
return False
|
|
|
|
|
|
|
|
owner = model.get_user(notification.repository.namespace)
|
|
|
|
if not owner:
|
|
|
|
# Something went wrong.
|
|
|
|
return False
|
|
|
|
|
|
|
|
url = 'https://api.flowdock.com/v1/messages/team_inbox/%s' % token
|
|
|
|
headers = {'Content-type': 'application/json'}
|
|
|
|
payload = {
|
|
|
|
'source': 'Quay',
|
|
|
|
'from_address': 'support@quay.io',
|
|
|
|
'subject': event_handler.get_summary(notification_data['event_data'], notification_data),
|
|
|
|
'content': event_handler.get_message(notification_data['event_data'], notification_data),
|
|
|
|
'from_name': owner.username,
|
|
|
|
'project': notification.repository.namespace + ' ' + notification.repository.name,
|
|
|
|
'tags': ['#' + event_handler.event_name()],
|
|
|
|
'link': notification_data['event_data']['homepage']
|
|
|
|
}
|
|
|
|
|
|
|
|
try:
|
|
|
|
resp = requests.post(url, data=json.dumps(payload), headers=headers)
|
|
|
|
if resp.status_code/100 != 2:
|
|
|
|
logger.error('%s response for flowdock to url: %s' % (resp.status_code,
|
|
|
|
url))
|
|
|
|
logger.error(resp.content)
|
|
|
|
return False
|
|
|
|
|
|
|
|
except requests.exceptions.RequestException as ex:
|
|
|
|
logger.exception('Flowdock method was unable to be sent: %s' % ex.message)
|
|
|
|
return False
|
|
|
|
|
|
|
|
return True
|
2014-08-19 21:40:36 +00:00
|
|
|
|
|
|
|
|
|
|
|
class HipchatMethod(NotificationMethod):
|
|
|
|
""" Method for sending notifications to Hipchat via the API:
|
|
|
|
https://www.hipchat.com/docs/apiv2/method/send_room_notification
|
|
|
|
"""
|
|
|
|
@classmethod
|
|
|
|
def method_name(cls):
|
|
|
|
return 'hipchat'
|
|
|
|
|
|
|
|
def validate(self, repository, config_data):
|
|
|
|
if not config_data.get('notification_token', ''):
|
|
|
|
raise CannotValidateNotificationMethodException('Missing Hipchat Room Notification Token')
|
|
|
|
|
|
|
|
if not config_data.get('room_id', ''):
|
|
|
|
raise CannotValidateNotificationMethodException('Missing Hipchat Room ID')
|
|
|
|
|
|
|
|
def perform(self, notification, event_handler, notification_data):
|
|
|
|
config_data = json.loads(notification.config_json)
|
|
|
|
|
|
|
|
token = config_data.get('notification_token', '')
|
|
|
|
room_id = config_data.get('room_id', '')
|
|
|
|
|
|
|
|
if not token or not room_id:
|
|
|
|
return False
|
|
|
|
|
|
|
|
owner = model.get_user(notification.repository.namespace)
|
|
|
|
if not owner:
|
|
|
|
# Something went wrong.
|
|
|
|
return False
|
|
|
|
|
|
|
|
url = 'https://api.hipchat.com/v2/room/%s/notification?auth_token=%s' % (room_id, token)
|
|
|
|
|
|
|
|
level = event_handler.get_level(notification_data['event_data'], notification_data)
|
|
|
|
color = {
|
|
|
|
'info': 'gray',
|
|
|
|
'warning': 'yellow',
|
|
|
|
'error': 'red',
|
|
|
|
'primary': 'purple'
|
|
|
|
}.get(level, 'gray')
|
|
|
|
|
|
|
|
headers = {'Content-type': 'application/json'}
|
|
|
|
payload = {
|
|
|
|
'color': color,
|
|
|
|
'message': event_handler.get_message(notification_data['event_data'], notification_data),
|
|
|
|
'notify': level == 'error',
|
|
|
|
'message_format': 'html',
|
|
|
|
}
|
|
|
|
|
|
|
|
try:
|
|
|
|
resp = requests.post(url, data=json.dumps(payload), headers=headers)
|
|
|
|
if resp.status_code/100 != 2:
|
|
|
|
logger.error('%s response for hipchat to url: %s' % (resp.status_code,
|
|
|
|
url))
|
|
|
|
logger.error(resp.content)
|
|
|
|
return False
|
|
|
|
|
|
|
|
except requests.exceptions.RequestException as ex:
|
|
|
|
logger.exception('Hipchat method was unable to be sent: %s' % ex.message)
|
|
|
|
return False
|
|
|
|
|
|
|
|
return True
|
2014-08-27 02:09:56 +00:00
|
|
|
|
|
|
|
|
|
|
|
class SlackMethod(NotificationMethod):
|
|
|
|
""" Method for sending notifications to Slack via the API:
|
|
|
|
https://api.slack.com/docs/attachments
|
|
|
|
"""
|
|
|
|
@classmethod
|
|
|
|
def method_name(cls):
|
|
|
|
return 'slack'
|
|
|
|
|
|
|
|
def validate(self, repository, config_data):
|
|
|
|
if not config_data.get('token', ''):
|
|
|
|
raise CannotValidateNotificationMethodException('Missing Slack Token')
|
|
|
|
|
|
|
|
if not config_data.get('subdomain', '').isalnum():
|
|
|
|
raise CannotValidateNotificationMethodException('Missing Slack Subdomain Name')
|
|
|
|
|
|
|
|
def formatForSlack(self, message):
|
|
|
|
message = message.replace('\n', '')
|
|
|
|
message = re.sub(r'\s+', ' ', message)
|
|
|
|
message = message.replace('<br>', '\n')
|
|
|
|
message = re.sub(r'<a href="(.+)">(.+)</a>', '<\\1|\\2>', message)
|
|
|
|
return message
|
|
|
|
|
|
|
|
def perform(self, notification, event_handler, notification_data):
|
|
|
|
config_data = json.loads(notification.config_json)
|
|
|
|
|
|
|
|
token = config_data.get('token', '')
|
|
|
|
subdomain = config_data.get('subdomain', '')
|
|
|
|
|
|
|
|
if not token or not subdomain:
|
|
|
|
return False
|
|
|
|
|
|
|
|
owner = model.get_user(notification.repository.namespace)
|
|
|
|
if not owner:
|
|
|
|
# Something went wrong.
|
|
|
|
return False
|
|
|
|
|
|
|
|
url = 'https://%s.slack.com/services/hooks/incoming-webhook?token=%s' % (subdomain, token)
|
|
|
|
|
|
|
|
level = event_handler.get_level(notification_data['event_data'], notification_data)
|
|
|
|
color = {
|
|
|
|
'info': '#ffffff',
|
|
|
|
'warning': 'warning',
|
|
|
|
'error': 'danger',
|
|
|
|
'primary': 'good'
|
|
|
|
}.get(level, '#ffffff')
|
|
|
|
|
|
|
|
summary = event_handler.get_summary(notification_data['event_data'], notification_data)
|
|
|
|
message = event_handler.get_message(notification_data['event_data'], notification_data)
|
|
|
|
|
|
|
|
headers = {'Content-type': 'application/json'}
|
|
|
|
payload = {
|
|
|
|
'text': summary,
|
|
|
|
'username': 'quayiobot',
|
|
|
|
'attachments': [
|
|
|
|
{
|
|
|
|
'fallback': summary,
|
|
|
|
'text': self.formatForSlack(message),
|
|
|
|
'color': color
|
|
|
|
}
|
|
|
|
]
|
|
|
|
}
|
|
|
|
|
|
|
|
try:
|
|
|
|
resp = requests.post(url, data=json.dumps(payload), headers=headers)
|
|
|
|
if resp.status_code/100 != 2:
|
|
|
|
logger.error('%s response for Slack to url: %s' % (resp.status_code,
|
|
|
|
url))
|
|
|
|
logger.error(resp.content)
|
|
|
|
return False
|
|
|
|
|
|
|
|
except requests.exceptions.RequestException as ex:
|
|
|
|
logger.exception('Slack method was unable to be sent: %s' % ex.message)
|
|
|
|
return False
|
|
|
|
|
|
|
|
return True
|