You've already forked oncall
mirror of
https://github.com/linkedin/oncall.git
synced 2025-11-30 23:44:59 +02:00
Clean/refactor notifier, add user validation (#12)
* Clean/refactor notifier, add user validation Fix a few notifier bugs, move reminder/user validator to their own files, add Makefile to docs * Move reminder/user validator to notifier directory
This commit is contained in:
@@ -53,8 +53,14 @@ notifications:
|
||||
- "email"
|
||||
|
||||
reminder:
|
||||
activated: True
|
||||
polling_interval: 360
|
||||
default_timezone: 'US/Pacific'
|
||||
|
||||
user_validator:
|
||||
activated: True
|
||||
subject: 'Warning: Missing phone number in Oncall'
|
||||
body: 'You are scheduled for an on-call shift in the future, but have no phone number recorded. Please update your information in Oncall.'
|
||||
|
||||
slack_instance: foobar
|
||||
header_color: '#3a3a3a'
|
||||
|
||||
20
docs/Makefile
Normal file
20
docs/Makefile
Normal file
@@ -0,0 +1,20 @@
|
||||
# Minimal makefile for Sphinx documentation
|
||||
#
|
||||
|
||||
# You can set these variables from the command line.
|
||||
SPHINXOPTS =
|
||||
SPHINXBUILD = sphinx-build
|
||||
SPHINXPROJ = Oncall
|
||||
SOURCEDIR = source
|
||||
BUILDDIR = build
|
||||
|
||||
# Put it first so that "make" without argument is like "make help".
|
||||
help:
|
||||
@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
|
||||
|
||||
.PHONY: help Makefile
|
||||
|
||||
# Catch-all target: route all unknown targets to Sphinx using the new
|
||||
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
|
||||
%: Makefile
|
||||
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
|
||||
1
setup.py
1
setup.py
@@ -31,6 +31,7 @@ setup(
|
||||
'pycrypto',
|
||||
'python-ldap',
|
||||
'pytz',
|
||||
'irisclient',
|
||||
],
|
||||
entry_points={
|
||||
'console_scripts': [
|
||||
|
||||
@@ -8,17 +8,12 @@ import yaml
|
||||
import logging
|
||||
import time
|
||||
from importlib import import_module
|
||||
from pytz import timezone
|
||||
from ujson import loads as json_loads, dumps as json_dumps
|
||||
from datetime import datetime
|
||||
from ujson import loads as json_loads
|
||||
from gevent import queue, spawn, sleep
|
||||
|
||||
from oncall import db, metrics, constants
|
||||
from oncall import db, metrics
|
||||
from oncall.messengers import init_messengers, send_message
|
||||
|
||||
HOUR = 60 * 60
|
||||
DAY = HOUR * 24
|
||||
WEEK = DAY * 7
|
||||
from oncall.notifier import reminder, user_validator
|
||||
|
||||
# logging
|
||||
logger = logging.getLogger()
|
||||
@@ -43,32 +38,6 @@ send_queue = queue.Queue()
|
||||
default_timezone = None
|
||||
|
||||
|
||||
def create_reminder(user_id, mode, send_time, context, type_name, cursor):
|
||||
context = json_dumps(context)
|
||||
cursor.execute('''INSERT INTO `notification_queue`(`user_id`, `send_time`, `mode_id`, `active`, `context`, `type_id`)
|
||||
VALUES (%s,
|
||||
%s,
|
||||
(SELECT `id` FROM `contact_mode` WHERE `name` = %s),
|
||||
1,
|
||||
%s,
|
||||
(SELECT `id` FROM `notification_type` WHERE `name` = %s))''',
|
||||
(user_id, send_time, mode, context, type_name))
|
||||
|
||||
|
||||
def timestamp_to_human_str(timestamp, tz):
|
||||
dt = datetime.fromtimestamp(timestamp, timezone(tz))
|
||||
return ' '.join([dt.strftime('%Y-%m-%d %H:%M:%S'), tz])
|
||||
|
||||
|
||||
def sec_to_human_str(seconds):
|
||||
if seconds % WEEK == 0:
|
||||
return '%d weeks' % (seconds / WEEK)
|
||||
elif seconds % DAY == 0:
|
||||
return '%d days' % (seconds / DAY)
|
||||
else:
|
||||
return '%d hours' % (seconds / HOUR)
|
||||
|
||||
|
||||
def load_config_file(config_path):
|
||||
with open(config_path) as h:
|
||||
config = yaml.load(h)
|
||||
@@ -167,71 +136,6 @@ def metrics_sender():
|
||||
sleep(60)
|
||||
|
||||
|
||||
def reminder(config):
|
||||
interval = config['polling_interval']
|
||||
default_timezone = config['default_timezone']
|
||||
|
||||
connection = db.connect()
|
||||
cursor = connection.cursor()
|
||||
cursor.execute('SELECT `last_window_end` FROM `notifier_state`')
|
||||
if cursor.rowcount != 1:
|
||||
window_start = int(time.time() - interval)
|
||||
logger.warning('Corrupted/missing notifier state; unable to determine last window. Guessing %s',
|
||||
window_start)
|
||||
else:
|
||||
window_start = cursor.fetchone()[0]
|
||||
|
||||
cursor.close()
|
||||
connection.close()
|
||||
|
||||
query = '''
|
||||
SELECT `user`.`name`, `user`.`id` AS `user_id`, `time_before`, `contact_mode`.`name` AS `mode`,
|
||||
`team`.`name` AS `team`, `event`.`start`, `event`.`id`, `role`.`name` AS `role`, `user`.`time_zone`
|
||||
FROM `user` JOIN `notification_setting` ON `notification_setting`.`user_id` = `user`.`id`
|
||||
AND `notification_setting`.`type_id` = (SELECT `id` FROM `notification_type`
|
||||
WHERE `name` = %s)
|
||||
JOIN `setting_role` ON `notification_setting`.`id` = `setting_role`.`setting_id`
|
||||
JOIN `event` ON `event`.`start` >= `time_before` + %s AND `event`.`start` < `time_before` + %s
|
||||
AND `event`.`user_id` = `user`.`id`
|
||||
AND `event`.`role_id` = `setting_role`.`role_id`
|
||||
AND `event`.`team_id` = `notification_setting`.`team_id`
|
||||
JOIN `contact_mode` ON `notification_setting`.`mode_id` = `contact_mode`.`id`
|
||||
JOIN `team` ON `event`.`team_id` = `team`.`id`
|
||||
JOIN `role` ON `event`.`role_id` = `role`.`id`
|
||||
LEFT JOIN `event` AS `e` ON `event`.`link_id` = `e`.`link_id` AND `e`.`start` < `event`.`start`
|
||||
WHERE `e`.`id` IS NULL
|
||||
'''
|
||||
|
||||
while(1):
|
||||
logger.info('Reminder polling loop started')
|
||||
window_end = int(time.time())
|
||||
|
||||
connection = db.connect()
|
||||
cursor = connection.cursor(db.DictCursor)
|
||||
|
||||
cursor.execute(query, (constants.ONCALL_REMINDER, window_start, window_end))
|
||||
notifications = cursor.fetchall()
|
||||
|
||||
for row in notifications:
|
||||
context = {'team': row['team'],
|
||||
'start_time': timestamp_to_human_str(row['start'],
|
||||
row['time_zone'] if row['time_zone'] else default_timezone),
|
||||
'time_before': sec_to_human_str(row['time_before']),
|
||||
'role': row['role']}
|
||||
create_reminder(row['user_id'], row['mode'], row['start'] - row['time_before'],
|
||||
context, 'oncall_reminder', cursor)
|
||||
logger.info('Created reminder with context %s for %s', context, row['name'])
|
||||
|
||||
cursor.execute('UPDATE `notifier_state` SET `last_window_end` = %s', window_end)
|
||||
connection.commit()
|
||||
logger.info('Created reminders for window [%s, %s), sleeping for %s s', window_start, window_end, interval)
|
||||
window_start = window_end
|
||||
|
||||
cursor.close()
|
||||
connection.close()
|
||||
sleep(interval)
|
||||
|
||||
|
||||
def main():
|
||||
with open(sys.argv[1], 'r') as config_file:
|
||||
config = yaml.safe_load(config_file)
|
||||
@@ -246,7 +150,10 @@ def main():
|
||||
init_messengers(config.get('messengers', []))
|
||||
|
||||
worker_tasks = [spawn(worker) for x in xrange(100)]
|
||||
spawn(reminder, config['reminder'])
|
||||
if config['reminder']['activated']:
|
||||
spawn(reminder.reminder, config['reminder'])
|
||||
if config['user_validator']['activated']:
|
||||
spawn(user_validator.user_validator, config['user_validator'])
|
||||
|
||||
interval = 60
|
||||
|
||||
|
||||
@@ -3,15 +3,14 @@
|
||||
|
||||
|
||||
from oncall.constants import EMAIL_SUPPORT, SMS_SUPPORT, CALL_SUPPORT
|
||||
from iris.client import IrisClient
|
||||
from irisclient import IrisClient
|
||||
|
||||
|
||||
class iris_messenger(object):
|
||||
supports = frozenset([EMAIL_SUPPORT, SMS_SUPPORT, CALL_SUPPORT])
|
||||
|
||||
def __init__(self, config):
|
||||
self.config = config
|
||||
self.iris_client = IrisClient(config['application'], config['iris_api_key'])
|
||||
self.iris_client = IrisClient(config['application'], config['iris_api_key'], config['api_host'])
|
||||
|
||||
def send(self, message):
|
||||
self.iris_client.notification(role='user', target=message['user'], priority=message.get('priority'),
|
||||
|
||||
0
src/oncall/notifier/__init__.py
Normal file
0
src/oncall/notifier/__init__.py
Normal file
104
src/oncall/notifier/reminder.py
Normal file
104
src/oncall/notifier/reminder.py
Normal file
@@ -0,0 +1,104 @@
|
||||
import time
|
||||
import logging
|
||||
from gevent import sleep
|
||||
from ujson import dumps as json_dumps
|
||||
from datetime import datetime
|
||||
from pytz import timezone
|
||||
from oncall import db, constants
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
HOUR = 60 * 60
|
||||
DAY = HOUR * 24
|
||||
WEEK = DAY * 7
|
||||
|
||||
|
||||
def create_reminder(user_id, mode, send_time, context, type_name, cursor):
|
||||
context = json_dumps(context)
|
||||
cursor.execute('''INSERT INTO `notification_queue`(`user_id`, `send_time`, `mode_id`, `active`, `context`, `type_id`)
|
||||
VALUES (%s,
|
||||
%s,
|
||||
(SELECT `id` FROM `contact_mode` WHERE `name` = %s),
|
||||
1,
|
||||
%s,
|
||||
(SELECT `id` FROM `notification_type` WHERE `name` = %s))''',
|
||||
(user_id, send_time, mode, context, type_name))
|
||||
|
||||
|
||||
def timestamp_to_human_str(timestamp, tz):
|
||||
dt = datetime.fromtimestamp(timestamp, timezone(tz))
|
||||
return ' '.join([dt.strftime('%Y-%m-%d %H:%M:%S'), tz])
|
||||
|
||||
|
||||
def sec_to_human_str(seconds):
|
||||
if seconds % WEEK == 0:
|
||||
return '%d weeks' % (seconds / WEEK)
|
||||
elif seconds % DAY == 0:
|
||||
return '%d days' % (seconds / DAY)
|
||||
else:
|
||||
return '%d hours' % (seconds / HOUR)
|
||||
|
||||
|
||||
def reminder(config):
|
||||
interval = config['polling_interval']
|
||||
default_timezone = config['default_timezone']
|
||||
|
||||
connection = db.connect()
|
||||
cursor = connection.cursor()
|
||||
cursor.execute('SELECT `last_window_end` FROM `notifier_state`')
|
||||
if cursor.rowcount != 1:
|
||||
window_start = int(time.time() - interval)
|
||||
logger.warning('Corrupted/missing notifier state; unable to determine last window. Guessing %s',
|
||||
window_start)
|
||||
else:
|
||||
window_start = cursor.fetchone()[0]
|
||||
|
||||
cursor.close()
|
||||
connection.close()
|
||||
|
||||
query = '''
|
||||
SELECT `user`.`name`, `user`.`id` AS `user_id`, `time_before`, `contact_mode`.`name` AS `mode`,
|
||||
`team`.`name` AS `team`, `event`.`start`, `event`.`id`, `role`.`name` AS `role`, `user`.`time_zone`
|
||||
FROM `user` JOIN `notification_setting` ON `notification_setting`.`user_id` = `user`.`id`
|
||||
AND `notification_setting`.`type_id` = (SELECT `id` FROM `notification_type`
|
||||
WHERE `name` = %s)
|
||||
JOIN `setting_role` ON `notification_setting`.`id` = `setting_role`.`setting_id`
|
||||
JOIN `event` ON `event`.`start` >= `time_before` + %s AND `event`.`start` < `time_before` + %s
|
||||
AND `event`.`user_id` = `user`.`id`
|
||||
AND `event`.`role_id` = `setting_role`.`role_id`
|
||||
AND `event`.`team_id` = `notification_setting`.`team_id`
|
||||
JOIN `contact_mode` ON `notification_setting`.`mode_id` = `contact_mode`.`id`
|
||||
JOIN `team` ON `event`.`team_id` = `team`.`id`
|
||||
JOIN `role` ON `event`.`role_id` = `role`.`id`
|
||||
LEFT JOIN `event` AS `e` ON `event`.`link_id` = `e`.`link_id` AND `e`.`start` < `event`.`start`
|
||||
WHERE `e`.`id` IS NULL
|
||||
'''
|
||||
|
||||
while(1):
|
||||
logger.info('Reminder polling loop started')
|
||||
window_end = int(time.time())
|
||||
|
||||
connection = db.connect()
|
||||
cursor = connection.cursor(db.DictCursor)
|
||||
|
||||
cursor.execute(query, (constants.ONCALL_REMINDER, window_start, window_end))
|
||||
notifications = cursor.fetchall()
|
||||
|
||||
for row in notifications:
|
||||
context = {'team': row['team'],
|
||||
'start_time': timestamp_to_human_str(row['start'],
|
||||
row['time_zone'] if row['time_zone'] else default_timezone),
|
||||
'time_before': sec_to_human_str(row['time_before']),
|
||||
'role': row['role']}
|
||||
create_reminder(row['user_id'], row['mode'], row['start'] - row['time_before'],
|
||||
context, 'oncall_reminder', cursor)
|
||||
logger.info('Created reminder with context %s for %s', context, row['name'])
|
||||
|
||||
cursor.execute('UPDATE `notifier_state` SET `last_window_end` = %s', window_end)
|
||||
connection.commit()
|
||||
logger.info('Created reminders for window [%s, %s), sleeping for %s s', window_start, window_end, interval)
|
||||
window_start = window_end
|
||||
|
||||
cursor.close()
|
||||
connection.close()
|
||||
sleep(interval)
|
||||
31
src/oncall/notifier/user_validator.py
Normal file
31
src/oncall/notifier/user_validator.py
Normal file
@@ -0,0 +1,31 @@
|
||||
import logging
|
||||
from gevent import sleep
|
||||
|
||||
|
||||
from oncall import db, messengers
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def user_validator(config):
|
||||
subject = config['subject']
|
||||
body = config['body']
|
||||
sleep_time = config.get('interval', 86400)
|
||||
while 1:
|
||||
connection = db.connect()
|
||||
cursor = connection.cursor()
|
||||
cursor.execute('''SELECT `user`.`name`
|
||||
FROM `event` LEFT JOIN `user_contact` ON `event`.`user_id` = `user_contact`.`user_id`
|
||||
AND `user_contact`.`mode_id` = (SELECT `id` FROM `contact_mode` WHERE `name` = 'call')
|
||||
JOIN `user` ON `event`.`user_id` = `user`.`id`
|
||||
WHERE `event`.`start` > UNIX_TIMESTAMP() AND `user_contact`.`destination` IS NULL
|
||||
GROUP BY `event`.`user_id`;''')
|
||||
for row in cursor:
|
||||
message = {'user': row[0],
|
||||
'mode': 'email',
|
||||
'subject': subject,
|
||||
'body': body}
|
||||
messengers.send_message(message)
|
||||
connection.close()
|
||||
cursor.close()
|
||||
sleep(sleep_time)
|
||||
Reference in New Issue
Block a user