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