1#!/usr/bin/env python 2# 3# Copyright (C) 2018 The Android Open Source Project 4# 5# Licensed under the Apache License, Version 2.0 (the "License"); 6# you may not use this file except in compliance with the License. 7# You may obtain a copy of the License at 8# 9# http://www.apache.org/licenses/LICENSE-2.0 10# 11# Unless required by applicable law or agreed to in writing, software 12# distributed under the License is distributed on an "AS IS" BASIS, 13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14# See the License for the specific language governing permissions and 15# limitations under the License. 16# 17 18import datetime 19import logging 20import re 21 22from google.appengine.api import app_identity 23from google.appengine.api import mail 24 25from webapp.src import vtslab_status as Status 26from webapp.src.proto import model 27from webapp.src.utils import datetime_util 28 29SENDER_ADDRESS = "noreply@{}.appspotmail.com" 30 31SEND_NOTIFICATION_FOOTER = ( 32 "You are receiving this email because you are " 33 "listed as an owner, or an administrator of the " 34 "lab {}.\nIf you received this email by mistake, " 35 "please send an email to VTS Lab infra development " 36 "team. Thank you.") 37 38SEND_DEVICE_NOTIFICATION_TITLE = ("[VTS lab] Devices not responding in lab {} " 39 "({})") 40SEND_DEVICE_NOTIFICATION_HEADER = "Devices in lab {} are not responding." 41 42SEND_JOB_NOTIFICATION_TITLE = ("[VTS lab] Job error has been occurred in " 43 "lab {} ({})") 44SEND_JOB_NOTIFICATION_HEADER = ("Jobs in lab {} have been completed " 45 "unexpectedly.") 46SEND_SCHEDULE_SUSPENSION_NOTIFICATION_TITLE = ( 47 "[VTS lab] A job schedule has been {}. ({})") 48SEND_SCHEDULE_SUSPENSION_NOTIFICATION_HEADER = ("The below job schedule has " 49 "been {}.") 50SEND_SCHEDULE_SUSPENSION_NOTIFICATION_FOOTER = ( 51 "You are receiving this email because one or more labs which you are " 52 "listed as an owner or an administrator are affected.\nIf you received " 53 "this email by mistake, please send an email to VTS Lab infra development " 54 "team. Thank you.") 55 56 57def send_device_notification(devices): 58 """Sends notification for not responding devices. 59 60 Args: 61 devices: a dict containing lab and host information of no-response 62 devices. 63 """ 64 for lab in devices: 65 email_message = mail.EmailMessage() 66 email_message.sender = SENDER_ADDRESS.format( 67 app_identity.get_application_id()) 68 try: 69 email_message.to = verify_recipient_address( 70 devices[lab]["_recipients"]) 71 except ValueError as e: 72 logging.error(e) 73 continue 74 email_message.subject = SEND_DEVICE_NOTIFICATION_TITLE.format( 75 lab, 76 datetime_util.GetTimeWithTimezone( 77 datetime.datetime.now()).strftime("%Y-%m-%d")) 78 message = "" 79 message += SEND_DEVICE_NOTIFICATION_HEADER.format(lab) 80 message += "\n\n" 81 for host in devices[lab]: 82 if host == "_recipients" or not devices[lab][host]: 83 continue 84 message += "hostname\n" 85 message += host 86 message += "\n\ndevices\n" 87 message += "\n".join(devices[lab][host]) 88 message += "\n\n\n" 89 message += "\n\n" 90 message += SEND_NOTIFICATION_FOOTER.format(lab) 91 92 try: 93 email_message.body = message 94 email_message.check_initialized() 95 email_message.send() 96 except mail.MissingRecipientError as e: 97 logging.exception(e) 98 99 100def send_job_notification(jobs): 101 """Sends notification for job error. 102 103 Args: 104 jobs: a JobModel entity, or a list of JobModel entities. 105 """ 106 if not jobs: 107 return 108 if type(jobs) is not list: 109 jobs = [jobs] 110 111 # grouping jobs by lab to send to each lab owner and admins at once. 112 labs_to_alert = {} 113 for job in jobs: 114 lab_query = model.LabModel.query( 115 model.LabModel.hostname == job.hostname) 116 labs = lab_query.fetch() 117 if labs: 118 lab = labs[0] 119 if lab.name not in labs_to_alert: 120 labs_to_alert[lab.name] = {} 121 labs_to_alert[lab.name]["jobs"] = [] 122 labs_to_alert[lab.name]["_recipients"] = [] 123 if lab.owner not in labs_to_alert[lab.name]["_recipients"]: 124 labs_to_alert[lab.name]["_recipients"].append(lab.owner) 125 labs_to_alert[lab.name]["_recipients"].extend([ 126 x for x in lab.admin 127 if x not in labs_to_alert[lab.name]["_recipients"] 128 ]) 129 labs_to_alert[lab.name]["jobs"].append(job) 130 else: 131 logging.warning( 132 "Could not find a lab model for hostname {}".format( 133 job.hostname)) 134 continue 135 136 for lab in labs_to_alert: 137 email_message = mail.EmailMessage() 138 email_message.sender = SENDER_ADDRESS.format( 139 app_identity.get_application_id()) 140 try: 141 email_message.to = verify_recipient_address( 142 labs_to_alert[lab]["_recipients"]) 143 except ValueError as e: 144 logging.error(e) 145 continue 146 email_message.subject = SEND_JOB_NOTIFICATION_TITLE.format( 147 lab, 148 datetime_util.GetTimeWithTimezone( 149 datetime.datetime.now()).strftime("%Y-%m-%d")) 150 message = "" 151 message += SEND_JOB_NOTIFICATION_HEADER.format(lab) 152 message += "\n\n" 153 message += "http://{}.appspot.com/job".format( 154 app_identity.get_application_id()) 155 message += "\n\n" 156 for job in labs_to_alert[lab]["jobs"]: 157 message += "hostname: {}\n\n".format(job.hostname) 158 message += "device: {}\n".format(job.device.split("/")[1]) 159 message += "device serial: {}\n".format(", ".join(job.serial)) 160 message += ( 161 "device: branch - {}, target - {}, build_id - {}\n").format( 162 job.manifest_branch, job.build_target, job.build_id) 163 message += "gsi: branch - {}, target - {}, build_id - {}\n".format( 164 job.gsi_branch, job.gsi_build_target, job.gsi_build_id) 165 message += "test: branch - {}, target - {}, build_id - {}\n".format( 166 job.test_branch, job.test_build_target, job.test_build_id) 167 message += "job created: {}\n".format( 168 datetime_util.GetTimeWithTimezone( 169 job.timestamp).strftime("%Y-%m-%d %H:%M:%S %Z")) 170 message += "job status: {}\n".format([ 171 key for key, value in Status.JOB_STATUS_DICT.items() 172 if value == job.status 173 ][0]) 174 message += "\n\n\n" 175 message += "\n\n" 176 message += SEND_NOTIFICATION_FOOTER.format(lab) 177 178 try: 179 email_message.body = message 180 email_message.check_initialized() 181 email_message.send() 182 except mail.MissingRecipientError as e: 183 logging.exception(e) 184 185 186def send_schedule_suspension_notification(schedule): 187 """Sends notification when a schedule is suspended, or resumed. 188 189 Args: 190 schedule: a ScheduleModel entity. 191 """ 192 if not schedule: 193 return 194 195 if not schedule.device: 196 return 197 198 email_message = mail.EmailMessage() 199 email_message.sender = SENDER_ADDRESS.format( 200 app_identity.get_application_id()) 201 202 lab_names = [] 203 for device in schedule.device: 204 if not "/" in device: 205 continue 206 lab_name = device.split("/")[0] 207 lab_names.append(lab_name) 208 209 recipients = [] 210 for lab_name in lab_names: 211 lab_query = model.LabModel.query(model.LabModel.name == lab_name) 212 labs = lab_query.fetch() 213 if labs: 214 lab = labs[0] 215 if lab.owner not in recipients: 216 recipients.append(lab.owner) 217 recipients.extend([x for x in lab.admin if x not in recipients]) 218 else: 219 logging.warning( 220 "Could not find a lab model for lab {}".format(lab_name)) 221 222 try: 223 email_message.to = verify_recipient_address(recipients) 224 except ValueError as e: 225 logging.error(e) 226 return 227 228 status_text = "suspended" if schedule.suspended else "resumed" 229 email_message.subject = SEND_SCHEDULE_SUSPENSION_NOTIFICATION_TITLE.format( 230 status_text, 231 datetime_util.GetTimeWithTimezone( 232 datetime.datetime.now()).strftime("%Y-%m-%d")) 233 message = "" 234 message += SEND_SCHEDULE_SUSPENSION_NOTIFICATION_HEADER.format(status_text) 235 message += "\n\n" 236 message += "\n\ndevices\n" 237 message += "\n".join(schedule.device) 238 message += "\n\ndevice branch\n" 239 message += schedule.manifest_branch 240 message += "\n\ndevice build target\n" 241 message += schedule.build_target 242 message += "\n\ngsi branch\n" 243 message += schedule.gsi_branch 244 message += "\n\ngsi build target\n" 245 message += schedule.gsi_build_target 246 message += "\n\ntest branch\n" 247 message += schedule.test_branch 248 message += "\n\ntest build target\n" 249 message += schedule.test_build_target 250 message += "\n\n" 251 message += ("Please see the details in the following link: " 252 "http://{}.appspot.com/schedule".format( 253 app_identity.get_application_id())) 254 message += "\n\n\n\n" 255 message += SEND_SCHEDULE_SUSPENSION_NOTIFICATION_FOOTER 256 257 try: 258 email_message.body = message 259 email_message.check_initialized() 260 email_message.send() 261 except mail.MissingRecipientError as e: 262 logging.exception(e) 263 264 265def verify_recipient_address(address): 266 """Verifies recipients address. 267 268 Args: 269 address: a list of strings or a string, recipient(s) address. 270 271 Returns: 272 A list of verified addresses if list type argument is given, or 273 a string of a verified address if str type argument is given. 274 275 Raises: 276 ValueError if type of address is neither list nor str. 277 """ 278 # pattern for 'any@google.com', and 'any name <any@google.com>' 279 verify_patterns = [ 280 re.compile(".*@google\.com$"), 281 re.compile(".*<.*@google\.com>$") 282 ] 283 if not address: 284 return None 285 if type(address) is list: 286 verified_address = [ 287 x for x in address 288 if any(pattern.match(x) for pattern in verify_patterns) 289 ] 290 return verified_address 291 elif type(address) is str: 292 return address if any( 293 pattern.match(address) for pattern in verify_patterns) else None 294 else: 295 raise ValueError("Wrong type - {}.".format(type(address))) 296