diff options
Diffstat (limited to 'cros_utils')
-rwxr-xr-x | cros_utils/command_executer_timeout_test.py | 37 | ||||
-rwxr-xr-x | cros_utils/command_executer_unittest.py | 2 | ||||
-rwxr-xr-x | cros_utils/email_sender.py | 113 | ||||
-rwxr-xr-x | cros_utils/email_sender_unittest.py | 120 | ||||
-rw-r--r-- | cros_utils/manifest_versions.py | 4 | ||||
-rwxr-xr-x | cros_utils/misc_test.py | 2 | ||||
-rwxr-xr-x | cros_utils/perf_diff.py | 4 | ||||
-rwxr-xr-x | cros_utils/tabulator_test.py | 2 | ||||
-rwxr-xr-x | cros_utils/timeline_test.py | 2 |
9 files changed, 274 insertions, 12 deletions
diff --git a/cros_utils/command_executer_timeout_test.py b/cros_utils/command_executer_timeout_test.py new file mode 100755 index 00000000..1c9c74cd --- /dev/null +++ b/cros_utils/command_executer_timeout_test.py @@ -0,0 +1,37 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# +# Copyright 2020 The Chromium OS Authors. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +"""Timeout test for command_executer.""" + +from __future__ import print_function + +__author__ = 'asharif@google.com (Ahmad Sharif)' + +import argparse +import sys + +from cros_utils import command_executer + + +def Usage(parser, message): + print('ERROR: %s' % message) + parser.print_help() + sys.exit(0) + + +def Main(argv): + parser = argparse.ArgumentParser() + _ = parser.parse_args(argv) + + command = 'sleep 1000' + ce = command_executer.GetCommandExecuter() + ce.RunCommand(command, command_timeout=1) + return 0 + + +if __name__ == '__main__': + Main(sys.argv[1:]) diff --git a/cros_utils/command_executer_unittest.py b/cros_utils/command_executer_unittest.py index 959000fd..22331ae0 100755 --- a/cros_utils/command_executer_unittest.py +++ b/cros_utils/command_executer_unittest.py @@ -11,7 +11,7 @@ from __future__ import print_function import time import unittest -import command_executer +from cros_utils import command_executer class CommandExecuterTest(unittest.TestCase): diff --git a/cros_utils/email_sender.py b/cros_utils/email_sender.py index 0019982e..6b8893ea 100755 --- a/cros_utils/email_sender.py +++ b/cros_utils/email_sender.py @@ -9,17 +9,35 @@ from __future__ import print_function -from email import encoders as Encoders -from email.mime.base import MIMEBase -from email.mime.multipart import MIMEMultipart -from email.mime.text import MIMEText +import base64 +import contextlib +import datetime import getpass +import json import os import smtplib import tempfile +from email import encoders as Encoders +from email.mime.base import MIMEBase +from email.mime.multipart import MIMEMultipart +from email.mime.text import MIMEText from cros_utils import command_executer +X20_PATH = '/google/data/rw/teams/c-compiler-chrome/prod_emails' + + +@contextlib.contextmanager +def AtomicallyWriteFile(file_path): + temp_path = file_path + '.in_progress' + try: + with open(temp_path, 'w') as f: + yield f + os.rename(temp_path, file_path) + except: + os.remove(temp_path) + raise + class EmailSender(object): """Utility class to send email through SMTP or SendGMR.""" @@ -31,6 +49,93 @@ class EmailSender(object): self.name = name self.content = content + def SendX20Email(self, + subject, + identifier, + well_known_recipients=(), + direct_recipients=(), + text_body=None, + html_body=None): + """Enqueues an email in our x20 outbox. + + These emails ultimately get sent by the machinery in + //depot/google3/googleclient/chrome/chromeos_toolchain/mailer/mail.go. This + kind of sending is intended for accounts that don't have smtp or gmr access + (e.g., role accounts), but can be used by anyone with x20 access. + + All emails are sent from `mdb.c-compiler-chrome+${identifier}@google.com`. + + Args: + subject: email subject. Must be nonempty. + identifier: email identifier, or the text that lands after the `+` in the + "From" email address. Must be nonempty. + well_known_recipients: a list of well-known recipients for the email. + These are translated into addresses by our mailer. + Current potential values for this are ('sheriff', + 'cwp-team', 'cros-team', 'mage'). Either this or + direct_recipients must be a nonempty list. + direct_recipients: @google.com emails to send addresses to. Either this + or well_known_recipients must be a nonempty list. + text_body: a 'text/plain' email body to send. Either this or html_body + must be a nonempty string. Both may be specified + html_body: a 'text/html' email body to send. Either this or text_body + must be a nonempty string. Both may be specified + """ + # `str`s act a lot like tuples/lists. Ensure that we're not accidentally + # iterating over one of those (or anything else that's sketchy, for that + # matter). + if not isinstance(well_known_recipients, (tuple, list)): + raise ValueError('`well_known_recipients` is unexpectedly a %s' % + type(well_known_recipients)) + + if not isinstance(direct_recipients, (tuple, list)): + raise ValueError( + '`direct_recipients` is unexpectedly a %s' % type(direct_recipients)) + + if not subject or not identifier: + raise ValueError('both `subject` and `identifier` must be nonempty') + + if not (well_known_recipients or direct_recipients): + raise ValueError('either `well_known_recipients` or `direct_recipients` ' + 'must be specified') + + for recipient in direct_recipients: + if not recipient.endswith('@google.com'): + raise ValueError('All recipients must end with @google.com') + + if not (text_body or html_body): + raise ValueError('either `text_body` or `html_body` must be specified') + + email_json = { + 'email_identifier': identifier, + 'subject': subject, + } + + if well_known_recipients: + email_json['well_known_recipients'] = well_known_recipients + + if direct_recipients: + email_json['direct_recipients'] = direct_recipients + + if text_body: + email_json['body'] = text_body + + if html_body: + email_json['html_body'] = html_body + + # The name of this has two parts: + # - An easily sortable time, to provide uniqueness and let our emailer + # send things in the order they were put into the outbox. + # - 64 bits of entropy, so two racing email sends don't clobber the same + # file. + now = datetime.datetime.utcnow().isoformat('T', 'seconds') + 'Z' + entropy = base64.urlsafe_b64encode(os.getrandom(8)) + entropy_str = entropy.rstrip(b'=').decode('utf-8') + result_path = os.path.join(X20_PATH, now + '_' + entropy_str + '.json') + + with AtomicallyWriteFile(result_path) as f: + json.dump(email_json, f) + def SendEmail(self, email_to, subject, diff --git a/cros_utils/email_sender_unittest.py b/cros_utils/email_sender_unittest.py new file mode 100755 index 00000000..73492196 --- /dev/null +++ b/cros_utils/email_sender_unittest.py @@ -0,0 +1,120 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +# Copyright 2020 The Chromium OS Authors. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +"""Tests for email_sender.""" + +from __future__ import print_function + +import contextlib +import io +import json +import unittest +import unittest.mock as mock + +import cros_utils.email_sender as email_sender + + +class Test(unittest.TestCase): + """Tests for email_sender.""" + + @mock.patch('cros_utils.email_sender.AtomicallyWriteFile') + def test_x20_email_sending_rejects_invalid_inputs(self, write_file): + test_cases = [ + { + # no subject + 'subject': '', + 'identifier': 'foo', + 'direct_recipients': ['gbiv@google.com'], + 'text_body': 'hi', + }, + { + 'subject': 'foo', + # no identifier + 'identifier': '', + 'direct_recipients': ['gbiv@google.com'], + 'text_body': 'hi', + }, + { + 'subject': 'foo', + 'identifier': 'foo', + # no recipients + 'direct_recipients': [], + 'text_body': 'hi', + }, + { + 'subject': 'foo', + 'identifier': 'foo', + 'direct_recipients': ['gbiv@google.com'], + # no body + }, + { + 'subject': 'foo', + 'identifier': 'foo', + # direct recipients lack @google. + 'direct_recipients': ['gbiv'], + 'text_body': 'hi', + }, + { + 'subject': 'foo', + 'identifier': 'foo', + # non-list recipients + 'direct_recipients': 'gbiv@google.com', + 'text_body': 'hi', + }, + { + 'subject': 'foo', + 'identifier': 'foo', + # non-list recipients + 'well_known_recipients': 'sheriff', + 'text_body': 'hi', + }, + ] + + sender = email_sender.EmailSender() + for case in test_cases: + with self.assertRaises(ValueError): + sender.SendX20Email(**case) + + write_file.assert_not_called() + + @mock.patch('cros_utils.email_sender.AtomicallyWriteFile') + def test_x20_email_sending_translates_to_reasonable_json(self, write_file): + written_obj = None + + @contextlib.contextmanager + def actual_write_file(file_path): + nonlocal written_obj + + self.assertTrue( + file_path.startswith(email_sender.X20_PATH + '/'), file_path) + f = io.StringIO() + yield f + written_obj = json.loads(f.getvalue()) + + write_file.side_effect = actual_write_file + email_sender.EmailSender().SendX20Email( + subject='hello', + identifier='world', + well_known_recipients=['sheriff'], + direct_recipients=['gbiv@google.com'], + text_body='text', + html_body='html', + ) + + self.assertEqual( + written_obj, { + 'subject': 'hello', + 'email_identifier': 'world', + 'well_known_recipients': ['sheriff'], + 'direct_recipients': ['gbiv@google.com'], + 'body': 'text', + 'html_body': 'html', + }) + + +if __name__ == '__main__': + unittest.main() diff --git a/cros_utils/manifest_versions.py b/cros_utils/manifest_versions.py index 83e88908..115c6046 100644 --- a/cros_utils/manifest_versions.py +++ b/cros_utils/manifest_versions.py @@ -16,8 +16,8 @@ import shutil import tempfile import time -import command_executer -import logger +from cros_utils import command_executer +from cros_utils import logger def IsCrosVersion(version): diff --git a/cros_utils/misc_test.py b/cros_utils/misc_test.py index cb7b8c89..21a545e9 100755 --- a/cros_utils/misc_test.py +++ b/cros_utils/misc_test.py @@ -14,7 +14,7 @@ __author__ = 'asharif@google.com (Ahmad Sharif)' import unittest # Local modules -import misc +from cros_utils import misc class UtilsTest(unittest.TestCase): diff --git a/cros_utils/perf_diff.py b/cros_utils/perf_diff.py index fed6a81d..b8ddb0c4 100755 --- a/cros_utils/perf_diff.py +++ b/cros_utils/perf_diff.py @@ -18,8 +18,8 @@ import functools import re import sys -import misc -import tabulator +from cros_utils import misc +from cros_utils import tabulator ROWS_TO_SHOW = 'Rows_to_show_in_the_perf_table' TOTAL_EVENTS = 'Total_events_of_this_profile' diff --git a/cros_utils/tabulator_test.py b/cros_utils/tabulator_test.py index 4c0f8677..227e2d70 100755 --- a/cros_utils/tabulator_test.py +++ b/cros_utils/tabulator_test.py @@ -14,7 +14,7 @@ __author__ = 'asharif@google.com (Ahmad Sharif)' import unittest # Local modules -import tabulator +from cros_utils import tabulator class TabulatorTest(unittest.TestCase): diff --git a/cros_utils/timeline_test.py b/cros_utils/timeline_test.py index 314cf3c4..8a10e549 100755 --- a/cros_utils/timeline_test.py +++ b/cros_utils/timeline_test.py @@ -13,7 +13,7 @@ __author__ = 'yunlian@google.com (Yunlian Jiang)' import time import unittest -import timeline +from cros_utils import timeline class TimeLineTest(unittest.TestCase): |