1#!/usr/bin/env python3
2
3"""Insert entries to the NOTIFICATIONS table.
4
5This is part of the old email system and substantially replaced by the new User/Roles
6system. Few if any of these notifications are still needed and the major exception -
7CHART-EPS AMSUA Unique Mode notifications - should be an Event instead.
8The job status change algorithm called by worker.py would be better handled inside
9role.py not here."""
10
11import logging
12from collections import OrderedDict
13
14from chart.db.connection import db_connect
15from chart.project import settings
16from chart.common import sendmail
17from chart.common.util import ensure_dir_exists
18from chart.common.util import nvl
19from chart.common.traits import name_of_thing
20from chart.project import SID
21from chart.web.user import User
22
23# connection also used for users, auth_user tables
24db_conn = db_connect('USERS')
25
26logger = logging.getLogger()
27user_logger = logging.getLogger('user')
28
29
30# We define all the available notification classes here.
31# Each notification must be an instance of one class.
32
33notification_classes = OrderedDict([
34 ('NO_INGESTION', {'description': 'Long gap in ingestion',
35 'has_state': True,
36 'has_parameter': True}),
37 # ('DISK_FULL', {}),
38 ('AOCS_SCAO17', {'description': 'Record problems in the AOCS SCAO17 algorithm',
39 'has_sid': True}),
40 ('SSR_POWER', {'description': 'Record problems in the SSR_POWER algorithm',
41 'has_sid': True}),
42 ('HOURLY_NOTIFICATION', {'description': 'Test notification',
43 'email': False}),
44 ('TEST_NOTIFICATION_1', {'description': 'Test notification'}),
45 ('TEST_NOTIFICATION_2', {'description': 'Test notification',
46 'has_sid': True,
47 'has_parameter': True}),
48 ('TEST_NOTIFICATION_3', {'description': 'Test notification',
49 'has_state': True,
50 'has_sid': True,
51 'has_parameter': True}),
52 # ('DATABASE_UNAVAILABLE', {'description': 'Cannot connect to database',
53 # 'has_state': True},
54 ('ACTIVITY_FAILED', {'description': 'An activity returned status of FAILED',
55 'has_sid': True,
56 'has_parameter': True,
57 'has_state': True}),
58 ('AMSUA_UNIQUE', {'description': 'AMSUA Unique mode data has been received',
59 'has_sid': True,
60 'has_state': True}),
61 ('SCHEDULER_STARTUP', {'description': 'Send when the scheduler starts to confirm it is '
62 'able to send notifications',
63 'has_sid': False,
64 'has_state': False}),
65 # ('GOME_TIMELINE_SEARCH', {'description': 'Debug messagesend after gome tl scan',
66 # 'has_sid': True}),
67 ('GOME_TIMELINE_ERROR', {'description': 'Debug messagesend after gome tl scan',
68 'has_sid': True}),
69 ])
70
71
72def statefile_name(notification, sid, param):
73 """Compute statefile name for this notification."""
74 dirname = settings.STATEFILE_DIR.child('notifications')
75 ensure_dir_exists(dirname)
76 if param is None:
77 param_part = ''
78
79 else:
80 param_part = '_{param}'.format(param=name_of_thing(param))
81
82 if notification_classes[notification].get('has_sid'):
83 # if sid is None:
84 # logger.warning('No SID set for notification which allows one')
85
86 return dirname.child('{classname}_{sid}{param}'.format(
87 classname=notification,
88 sid=SID.get_reportname_part(sid),
89 param=param_part))
90
91 else:
92 if sid is not None:
93 logger.warning('Attempt to set SID for a notification which doesn\'t allow it')
94
95 return dirname.child('{classname}_{param}'.format(
96 classname=notification,
97 param=param_part))
98
99
100def set_statefile(notification, sid, param): # , not_id):
101 """Create notification statefile if it doesn't exist."""
102 path = statefile_name(notification, sid, param)
103 if not path.exists():
104 logging.debug('Creating statefile {path}'.format(path=path))
105 with open(str(path), 'w') as handle:
106 handle.write(' ') # str(not_id))
107
108 else:
109 logging.debug('Not creating statefile {path}'.format(path=path))
110
111
112def clear_statefile(notification, sid, param):
113 """Remove notification statefile if it exists."""
114 path = statefile_name(notification, sid, param)
115 if path.exists():
116 logging.debug('Removing statefile {path}'.format(path=path))
117 path.unlink()
118
119 else:
120 logging.debug('No statefile {path}'.format(path=path))
121
122
123def test_statefile(notification, sid, param):
124 """Check if notification statefile exists."""
125 path = statefile_name(notification, sid, param)
126 res = path.exists()
127 if not res:
128 # logger.debug('Notification statefile {name} does not exist'.format(name=path.name))
129 return False
130
131 else:
132 logger.debug('Notification statefile {name} exists'.format(name=path.name))
133 # not_id = int(open(str(path), 'r').read())
134 # logger.debug('Notification statefile {name} has id {id}'.format(
135 # name=path.name, id=not_id))
136 # return not_id
137 return True
138
139
140def notify(notification_classname,
141 message='No message set',
142 sid=None,
143 parameter=None):
144 """Create a notification.
145 Emails are sent to subscribers and a statefile is created
146 if the notification class has the 'has_state' attribute.
147 `message` :: Appears in the body of the email message. It is purely for information.
148 `sid` :: Should be None for system jobs, otherwise the relevant SID
149 `parameter` :: Can be a short string. It appears in the email subject.
150 For notifications with a state, the same `parameter` must be passed to the `denotify`
151 call to end the notification.
152 Statefull notifications do not have to have a parameter.
153
154 Notifications are just logged if settings.DEBUG is set.
155 """
156
157 # Rules for sending notifications:
158 # - We want notifications for NRT data on the OPE or VAL systems.
159 # - We don't want notifications from from TEST or DEV (?)
160 # - No notifications if ingester, dispatcher, jobcontrol etc. are being run from the command
161 # line.
162
163 notification_class = notification_classes.get(notification_classname)
164 if notification_class is None:
165 logger.error('notify() called with unknown class {cls}'.format(cls=notification_classname))
166 return
167
168 # check for required function arguments
169 if notification_class.get('has_parameter', False) and parameter is None:
170 logger.error('Attempt to raise notification {cls} with no parameter set'.format(
171 cls=notification_classname))
172 return
173
174 # if notification_class.get('has_sid', False) and sid is None:
175 # logger.error('Attempt to raise notification {cls} with no SID set'.format(
176 # cls=notification_classname))
177 # return
178
179 if settings.DEBUG:
180 logger.info('Not emailing as settings.DEBUG is set notification {cls} {sid} {param} '
181 '{msg}'.format(cls=notification_classname,
182 sid=sid,
183 param=parameter,
184 msg=message))
185 return
186
187 # statefull notification, see if already set
188 if notification_class.get('has_state'):
189 if test_statefile(notification_classname, sid, parameter):
190 logger.info('Not sending email as notification statefile file exists')
191 return
192
193 # record the notification ID in a statefile
194 set_statefile(notification_classname, sid, parameter)
195
196 user_logger.info('Notification {msg}'.format(msg=message))
197
198 if settings.DEBUG is True:
199 # don't send out emails in debug mode
200 logger.debug('Not sending email because settings.DEBUG is true')
201 return
202
203 if not notification_class.get('email', True):
204 # don't send emails if {'email':False} is set
205 logger.debug('Not sending email because this notification has the `email` '
206 'attribute set to false')
207 return
208
209 subject = notification_classname
210 if parameter is not None:
211 subject += ' ' + parameter
212
213 if sid is not None:
214 subject += ' for {sid}'.format(sid=sid)
215
216 notification_email(
217 subject=subject,
218 html="""Class: <b>{classname}</b><br>
219SID: {sid}<br>
220Parameter: {param}<br>
221Message:<br>
222{mess}<br>
223<br>
224{suffix}""".format(classname=notification_classname,
225 sid='n/a' if sid is None else '<b>{sid}</b>'.format(sid=sid),
226 param='n/a' if parameter is None else '<b>' + parameter + '</b>',
227 mess=message,
228 suffix=settings.EMAIL_MESSAGE_SUFFIX))
229
230
231def denotify(notification_classname, parameter=None, sid=None):
232 """Show that an error state, previously created by a call to notify()
233 with a `statefile` parameter, has ended."""
234 notification_class = notification_classes.get(notification_classname)
235 if notification_class is None:
236 logger.error('Denotify called with unknown class {cls}'.format(cls=notification_classname))
237 return
238
239 if not test_statefile(notification_classname, sid, parameter):
240 return
241
242 clear_statefile(notification_classname, sid, parameter)
243
244 logger.info('End of notification state {cls} {param}'.format(
245 cls=notification_classname, param=parameter))
246
247 if settings.DEBUG:
248 logger.info('Not sending denotification email as settings.DEBUG is set')
249 return
250
251 if not notification_class.get('email', True):
252 # don't send emails if {'email':False} is set
253 logger.debug('Not sending email because this notification has the `email` '
254 'attribute set to false')
255 return
256
257 notification_email(subject='End of state {cls}'.format(cls=notification_classname),
258 html="""A notification state has ended.
259Class {classname}
260SID: {sid}
261Parameter: {param}
262{suffix}""".format( # start='not implemented',
263 classname=notification_classname,
264 sid=sid,
265 param=parameter,
266 suffix=settings.EMAIL_MESSAGE_SUFFIX))
267
268 # db_conn.query('UPDATE notifications SET stop_time=:stop_time WHERE id=:id',
269 # id=not_id,
270 # stop_time=datetime.utcnow())
271 # db_conn.commit()
272
273
274def notification_email(subject,
275 message=None,
276 html=None):
277 """Internal function to this module.
278 After a notification has been entered into the NOTIFICATIONS table
279 test for any emails that should be sent.
280 """
281
282 # Subject: first message
283 # This message was sent at {local} local time.
284 # The following notifications have been raised:
285 # The following notifications have been closed:
286 # best regards, CHART
287
288 # body = None
289
290 if settings.EMAIL_HOST is None:
291 logger.info('Not sending email as EMAIL_HOST is None')
292 return
293
294 for target in (settings.ADMINS if settings.SYSADMINS is None else settings.SYSADMINS):
295 if isinstance(target, str):
296 # just email address
297 name = email = target
298
299 else:
300 # (full name, email address)
301 name, email = target
302
303 logger.info('Emailing {name} <{email}>'.format(
304 name=name,
305 email=email))
306
307 from_address = (settings.EMAIL_NAME.format('notify'),
308 settings.EMAIL_ADDRESS.format('notify'))
309
310 if message is not None:
311 sendmail.sendmail(
312 from_address=from_address,
313 to_addresses=((name, email),),
314 subject=settings.EMAIL_SUBJECT_PREFIX + subject,
315 message=message)
316
317 else:
318 sendmail.sendmail_html(
319 from_address=from_address,
320 to_addresses=((name, email),),
321 subject=settings.EMAIL_SUBJECT_PREFIX + subject,
322 html=html)
323
324
325def main():
326 """Command line entry point."""
327
328 from chart.common.args import ArgumentParser
329 parser = ArgumentParser()
330 parser.add_argument('--db', '--conn', '-d',
331 metavar='CONN',
332 help='Use database connection CONNECTION')
333 parser.add_argument('--notify-1',
334 help='Create a test notification',
335 action='store_true')
336 parser.add_argument('--notify-2',
337 help='Create a test notification with parameters',
338 action='store_true')
339 parser.add_argument('--notify-3',
340 help='Create a test notification with duration',
341 action='store_true')
342 parser.add_argument('--denotify',
343 help='Create a test denotification',
344 action='store_true')
345 parser.add_argument('--unique',
346 action='store_true')
347 parser.add_argument('--non-unique',
348 action='store_true')
349 args = parser.parse_args()
350
351 if args.db:
352 settings.set_db_name(args.db)
353
354 if args.unique:
355 notify('AMSUA_UNIQUE',
356 message='AMSU-A unique mode data found',
357 sid=SID('M02'))
358 parser.exit()
359
360 if args.non_unique:
361 denotify('AMSUA_UNIQUE', sid=SID('M02'))
362 parser.exit()
363
364 if args.notify_1:
365 logger.info('Sending notification 1')
366 notify('TEST_NOTIFICATION_1',
367 message='This is a test notification with no parameters')
368
369 elif args.notify_2:
370 logger.info('Sending notification 2')
371 notify('TEST_NOTIFICATION_2',
372 sid=SID('M02'),
373 parameter='TESTPARAM',
374 message='This is a test notification with SID and a parameter')
375
376 elif args.notify_3:
377 logger.info('Sending notification 3')
378 notify('TEST_NOTIFICATION_3',
379 sid=SID('M02'),
380 parameter='TESTPARAM',
381 message='This is a test notification with a duration')
382
383 elif args.denotify:
384 logger.info('Sending denotification 3')
385 denotify('TEST_NOTIFICATION_3',
386 sid=SID('M02'),
387 parameter='TESTPARAM')
388
389 else:
390 parser.error('No actions specified')
391
392if __name__ == '__main__':
393 main()