xref: /aosp_15_r20/external/autotest/contrib/upload_results.py (revision 9c5db1993ded3edbeafc8092d69fe5de2ee02df7)
1*9c5db199SXin Li#!/usr/bin/env python3
2*9c5db199SXin Li# -*- coding: utf-8 -*-
3*9c5db199SXin Li# Copyright 2020 The Chromium OS Authors. All rights reserved.
4*9c5db199SXin Li# Use of this source code is governed by a BSD-style license that can be
5*9c5db199SXin Li# found in the LICENSE file.
6*9c5db199SXin Li
7*9c5db199SXin Liimport argparse
8*9c5db199SXin Liimport logging as log
9*9c5db199SXin Liimport os
10*9c5db199SXin Liimport re
11*9c5db199SXin Liimport shlex
12*9c5db199SXin Liimport shutil
13*9c5db199SXin Liimport subprocess
14*9c5db199SXin Liimport multiprocessing
15*9c5db199SXin Liimport sys
16*9c5db199SXin Liimport time
17*9c5db199SXin Liimport uuid
18*9c5db199SXin Liimport json
19*9c5db199SXin Liimport functools
20*9c5db199SXin Liimport glob
21*9c5db199SXin Li
22*9c5db199SXin Lifrom google.cloud import storage
23*9c5db199SXin Lifrom google.api_core import exceptions as cloud_exceptions
24*9c5db199SXin Li# pylint: disable=no-name-in-module, import-error
25*9c5db199SXin Li
26*9c5db199SXin Liimport common
27*9c5db199SXin Lifrom autotest_lib.client.common_lib import global_config
28*9c5db199SXin Lifrom autotest_lib.client.common_lib import mail, pidfile
29*9c5db199SXin Lifrom autotest_lib.tko.parse import parse_one, export_tko_job_to_file
30*9c5db199SXin Li
31*9c5db199SXin Li# Appends the moblab source paths for the pubsub wrapper
32*9c5db199SXin Lisys.path.append('/mnt/host/source/src/platform/moblab/src')
33*9c5db199SXin Lifrom moblab_common import pubsub_client
34*9c5db199SXin Li
35*9c5db199SXin LiSTATUS_FILE = "status"
36*9c5db199SXin LiSTATUS_LOG_FILE = "status.log"
37*9c5db199SXin LiKEYVAL_FILE = "keyval"
38*9c5db199SXin LiNEW_KEYVAL_FILE = "new_keyval"
39*9c5db199SXin LiUPLOADED_STATUS_FILE = ".uploader_status"
40*9c5db199SXin LiSTATUS_GOOD = "PUBSUB_SENT"
41*9c5db199SXin LiFAKE_MOBLAB_ID_FILE = "fake_moblab_id_do_not_delete.txt"
42*9c5db199SXin LiGIT_HASH_FILE = "git_hash.txt"
43*9c5db199SXin LiGIT_COMMAND = ("git log --pretty=format:'%h -%d %s (%ci) <%an>'"
44*9c5db199SXin Li               " --abbrev-commit -20")
45*9c5db199SXin LiAUTOTEST_DIR = "/mnt/host/source/src/third_party/autotest/files/"
46*9c5db199SXin LiDEFAULT_SUITE_NAME = "default_suite"
47*9c5db199SXin LiSUITE_NAME_REGEX = "Fetching suite for suite named (.+?)\.\.\."
48*9c5db199SXin LiDEBUG_FILE_PATH = "debug/test_that.DEBUG"
49*9c5db199SXin LiCONFIG_DIR = os.path.dirname(os.path.abspath(__file__)) + "/config/"
50*9c5db199SXin LiDEFAULT_BOTO_CONFIG = CONFIG_DIR + ".boto_upload_utils"
51*9c5db199SXin LiUPLOAD_CONFIG = CONFIG_DIR + "upload_config.json"
52*9c5db199SXin LiSERVICE_ACCOUNT_CONFIG = CONFIG_DIR + ".service_account.json"
53*9c5db199SXin Li
54*9c5db199SXin Lilogging = log.getLogger(__name__)
55*9c5db199SXin Li
56*9c5db199SXin Li
57*9c5db199SXin Lidef parse_arguments(argv):
58*9c5db199SXin Li    """Creates the argument parser.
59*9c5db199SXin Li
60*9c5db199SXin Li    Args:
61*9c5db199SXin Li        argv: A list of input arguments.
62*9c5db199SXin Li
63*9c5db199SXin Li    Returns:
64*9c5db199SXin Li        A parser object for input arguments.
65*9c5db199SXin Li    """
66*9c5db199SXin Li    parser = argparse.ArgumentParser(description=__doc__)
67*9c5db199SXin Li    subparsers = parser.add_subparsers(
68*9c5db199SXin Li            help='select sub option for test result utility',
69*9c5db199SXin Li            dest='subcommand')
70*9c5db199SXin Li    subparsers.required = True
71*9c5db199SXin Li    parser.add_argument("-v",
72*9c5db199SXin Li                        "--verbose",
73*9c5db199SXin Li                        dest='verbose',
74*9c5db199SXin Li                        action='store_true',
75*9c5db199SXin Li                        help="Enable verbose (debug) logging.")
76*9c5db199SXin Li    parser.add_argument("-q",
77*9c5db199SXin Li                        "--quiet",
78*9c5db199SXin Li                        dest='quiet',
79*9c5db199SXin Li                        action='store_true',
80*9c5db199SXin Li                        help="Quiet mode for background call")
81*9c5db199SXin Li    def_logfile = "/tmp/" + os.path.basename(
82*9c5db199SXin Li            sys.argv[0].split(".")[0]) + ".log"
83*9c5db199SXin Li    parser.add_argument("-l",
84*9c5db199SXin Li                        "--logfile",
85*9c5db199SXin Li                        type=str,
86*9c5db199SXin Li                        required=False,
87*9c5db199SXin Li                        default=def_logfile,
88*9c5db199SXin Li                        help="Full path to logfile. Default: " + def_logfile)
89*9c5db199SXin Li
90*9c5db199SXin Li    # configuration subcommand to create config file and populate environment
91*9c5db199SXin Li    config_parser = subparsers.add_parser(name="config",
92*9c5db199SXin Li                                          help='upload test results to CPCon')
93*9c5db199SXin Li    config_parser.add_argument(
94*9c5db199SXin Li            "-b",
95*9c5db199SXin Li            "--bucket",
96*9c5db199SXin Li            type=str,
97*9c5db199SXin Li            required=True,
98*9c5db199SXin Li            help="The GCS bucket that test results are uploaded to, e.g."
99*9c5db199SXin Li            "'gs://xxxx'.")
100*9c5db199SXin Li    config_parser.add_argument("-f",
101*9c5db199SXin Li                               "--force",
102*9c5db199SXin Li                               dest='force',
103*9c5db199SXin Li                               action="store_true",
104*9c5db199SXin Li                               help="Force overwrite of previous config files")
105*9c5db199SXin Li
106*9c5db199SXin Li    upload_parser = subparsers.add_parser(name="upload",
107*9c5db199SXin Li                                          help='upload test results to CPCon')
108*9c5db199SXin Li    upload_parser.add_argument(
109*9c5db199SXin Li            "--bug",
110*9c5db199SXin Li            type=_valid_bug_id,
111*9c5db199SXin Li            required=False,
112*9c5db199SXin Li            help=
113*9c5db199SXin Li            "Write bug id to the test results. Each test entry can only have "
114*9c5db199SXin Li            "at most 1 bug id. Optional.")
115*9c5db199SXin Li    upload_parser.add_argument(
116*9c5db199SXin Li            "-d",
117*9c5db199SXin Li            "--directory",
118*9c5db199SXin Li            type=str,
119*9c5db199SXin Li            required=True,
120*9c5db199SXin Li            help="The directory of non-Moblab test results.")
121*9c5db199SXin Li    upload_parser.add_argument(
122*9c5db199SXin Li            "--parse_only",
123*9c5db199SXin Li            action='store_true',
124*9c5db199SXin Li            help="Generate job.serialize locally but do not upload test "
125*9c5db199SXin Li            "directories and not send pubsub messages.")
126*9c5db199SXin Li    upload_parser.add_argument(
127*9c5db199SXin Li            "--upload_only",
128*9c5db199SXin Li            action='store_true',
129*9c5db199SXin Li            help="Leave existing protobuf files as-is, only upload "
130*9c5db199SXin Li            "directories and send pubsub messages.")
131*9c5db199SXin Li    upload_parser.add_argument(
132*9c5db199SXin Li            "-f",
133*9c5db199SXin Li            "--force",
134*9c5db199SXin Li            dest='force',
135*9c5db199SXin Li            action='store_true',
136*9c5db199SXin Li            help=
137*9c5db199SXin Li            "force re-upload of results even if results were already successfully uploaded."
138*9c5db199SXin Li    )
139*9c5db199SXin Li    upload_parser.add_argument(
140*9c5db199SXin Li            "-s",
141*9c5db199SXin Li            "--suite",
142*9c5db199SXin Li            type=str,
143*9c5db199SXin Li            default=None,
144*9c5db199SXin Li            help="The suite is used to identify the type of test results,"
145*9c5db199SXin Li            "e.g. 'power' for platform power team. If not specific, the "
146*9c5db199SXin Li            "default value is 'default_suite'.")
147*9c5db199SXin Li    return parser.parse_args(argv)
148*9c5db199SXin Li
149*9c5db199SXin Li
150*9c5db199SXin Lidef _confirm_option(question):
151*9c5db199SXin Li    """
152*9c5db199SXin Li        Get a yes/no answer from the user via command line.
153*9c5db199SXin Li
154*9c5db199SXin Li    Args:
155*9c5db199SXin Li        question: string, question to ask the user.
156*9c5db199SXin Li
157*9c5db199SXin Li    Returns:
158*9c5db199SXin Li        A boolean. True if yes; False if no.
159*9c5db199SXin Li    """
160*9c5db199SXin Li    expected_answers = ['y', 'yes', 'n', 'no']
161*9c5db199SXin Li    answer = ''
162*9c5db199SXin Li    while answer not in expected_answers:
163*9c5db199SXin Li        answer = input(question + "(y/n): ").lower().strip()
164*9c5db199SXin Li    return answer[0] == "y"
165*9c5db199SXin Li
166*9c5db199SXin Li
167*9c5db199SXin Lidef _read_until_string(pipe, stop_string):
168*9c5db199SXin Li    lines = [""]
169*9c5db199SXin Li    while True:
170*9c5db199SXin Li        c = pipe.read(1)
171*9c5db199SXin Li        lines[-1] = lines[-1] + c.decode("utf-8")
172*9c5db199SXin Li        if stop_string == lines[-1]:
173*9c5db199SXin Li            return lines
174*9c5db199SXin Li        if c.decode("utf-8") == "\n":
175*9c5db199SXin Li            lines.append("")
176*9c5db199SXin Li
177*9c5db199SXin Li
178*9c5db199SXin Lidef _configure_environment(parsed_args):
179*9c5db199SXin Li    # create config directory if not exists
180*9c5db199SXin Li    os.makedirs(CONFIG_DIR, exist_ok=True)
181*9c5db199SXin Li
182*9c5db199SXin Li    if os.path.exists(UPLOAD_CONFIG) and not parsed_args.force:
183*9c5db199SXin Li        logging.error("Environment already configured, run with --force")
184*9c5db199SXin Li        exit(1)
185*9c5db199SXin Li
186*9c5db199SXin Li    # call the gsutil config tool to set up accounts
187*9c5db199SXin Li    if os.path.exists(DEFAULT_BOTO_CONFIG + ".bak"):
188*9c5db199SXin Li        os.remove(DEFAULT_BOTO_CONFIG + ".bak")
189*9c5db199SXin Li
190*9c5db199SXin Li    if os.path.exists(DEFAULT_BOTO_CONFIG):
191*9c5db199SXin Li        os.remove(DEFAULT_BOTO_CONFIG)
192*9c5db199SXin Li    os.mknod(DEFAULT_BOTO_CONFIG)
193*9c5db199SXin Li    os.environ["BOTO_CONFIG"] = DEFAULT_BOTO_CONFIG
194*9c5db199SXin Li    os.environ[
195*9c5db199SXin Li            "GOOGLE_APPLICATION_CREDENTIALS"] = CONFIG_DIR + ".service_account.json"
196*9c5db199SXin Li    with subprocess.Popen(["gsutil", "config"],
197*9c5db199SXin Li                          stdout=subprocess.PIPE,
198*9c5db199SXin Li                          stderr=subprocess.PIPE,
199*9c5db199SXin Li                          stdin=subprocess.PIPE) as sp:
200*9c5db199SXin Li        lines = _read_until_string(sp.stdout, "Enter the authorization code: ")
201*9c5db199SXin Li        code = input("enter auth code from " + str(lines[1]) + ": ")
202*9c5db199SXin Li        sp.stdin.write(bytes(code + '\n', "utf-8"))
203*9c5db199SXin Li        sp.stdin.flush()
204*9c5db199SXin Li        lines = _read_until_string(sp.stdout, "What is your project-id? ")
205*9c5db199SXin Li        sp.stdin.write(bytes(parsed_args.bucket + '\n', "utf-8"))
206*9c5db199SXin Li        sp.stdin.flush()
207*9c5db199SXin Li
208*9c5db199SXin Li    subprocess.run([
209*9c5db199SXin Li            "gsutil", "cp",
210*9c5db199SXin Li            "gs://" + parsed_args.bucket + "/.service_account.json", CONFIG_DIR
211*9c5db199SXin Li    ])
212*9c5db199SXin Li    subprocess.run([
213*9c5db199SXin Li            "gsutil", "cp",
214*9c5db199SXin Li            "gs://" + parsed_args.bucket + "/pubsub-key-do-not-delete.json",
215*9c5db199SXin Li            CONFIG_DIR
216*9c5db199SXin Li    ])
217*9c5db199SXin Li
218*9c5db199SXin Li    sa_filename = ""
219*9c5db199SXin Li    if os.path.exists(CONFIG_DIR + "/.service_account.json"):
220*9c5db199SXin Li        sa_filename = ".service_account.json"
221*9c5db199SXin Li    elif os.path.exists(CONFIG_DIR + "/pubsub-key-do-not-delete.json"):
222*9c5db199SXin Li        sa_filename = "pubsub-key-do-not-delete.json"
223*9c5db199SXin Li    else:
224*9c5db199SXin Li        logging.error("No pubsub key found in bucket, failed config!")
225*9c5db199SXin Li        exit(1)
226*9c5db199SXin Li
227*9c5db199SXin Li    # deposit parsed_args.bucket to the json file
228*9c5db199SXin Li    with open(UPLOAD_CONFIG, "w") as cf:
229*9c5db199SXin Li        settings = {}
230*9c5db199SXin Li        settings["bucket"] = parsed_args.bucket
231*9c5db199SXin Li        settings["service_account"] = CONFIG_DIR + sa_filename
232*9c5db199SXin Li        settings["boto_key"] = DEFAULT_BOTO_CONFIG
233*9c5db199SXin Li
234*9c5db199SXin Li        cf.write(json.dumps(settings))
235*9c5db199SXin Li
236*9c5db199SXin Li
237*9c5db199SXin Lidef _load_config():
238*9c5db199SXin Li    mandatory_keys = ["bucket", "service_account", "boto_key"]
239*9c5db199SXin Li
240*9c5db199SXin Li    if not os.path.exists(UPLOAD_CONFIG):
241*9c5db199SXin Li        logging.error("Missing mandatory config file, run config command")
242*9c5db199SXin Li        exit(1)
243*9c5db199SXin Li    with open(UPLOAD_CONFIG, "r") as cf:
244*9c5db199SXin Li        settings = json.load(cf)
245*9c5db199SXin Li
246*9c5db199SXin Li    for key in mandatory_keys:
247*9c5db199SXin Li        if key not in settings:
248*9c5db199SXin Li            logging.error("Missing mandatory setting " + str(key) +
249*9c5db199SXin Li                          ", run config command")
250*9c5db199SXin Li            exit()
251*9c5db199SXin Li
252*9c5db199SXin Li    os.environ["GOOGLE_APPLICATION_CREDENTIALS"] = settings["service_account"]
253*9c5db199SXin Li    os.environ["BOTO_CONFIG"] = settings["boto_key"]
254*9c5db199SXin Li    return argparse.Namespace(**settings)
255*9c5db199SXin Li
256*9c5db199SXin Li
257*9c5db199SXin Liclass ResultsManager:
258*9c5db199SXin Li    def __init__(self, results_parser, results_sender):
259*9c5db199SXin Li        self.parent_directories = []
260*9c5db199SXin Li        self.result_directories = set()
261*9c5db199SXin Li        self.results = []
262*9c5db199SXin Li        self.results_parser = results_parser
263*9c5db199SXin Li        self.results_sender = results_sender
264*9c5db199SXin Li        self.bug_id = None
265*9c5db199SXin Li        self.suite_name = ""
266*9c5db199SXin Li
267*9c5db199SXin Li        self.moblab_id = self.get_fake_moblab_id()
268*9c5db199SXin Li
269*9c5db199SXin Li    def new_directory(self, parent_dir: str):
270*9c5db199SXin Li        self.parent_directories.append(parent_dir)
271*9c5db199SXin Li
272*9c5db199SXin Li    def enumerate_all_directories(self):
273*9c5db199SXin Li        self.result_directories = set()
274*9c5db199SXin Li        for parent_dir in self.parent_directories:
275*9c5db199SXin Li            self.enumerate_result_directories(parent_dir)
276*9c5db199SXin Li
277*9c5db199SXin Li    def enumerate_result_directories(self, parent_dir):
278*9c5db199SXin Li        """ Gets all test directories.
279*9c5db199SXin Li
280*9c5db199SXin Li        Args:
281*9c5db199SXin Li        parent_dir: The parent directory of one or multiple test directories
282*9c5db199SXin Li
283*9c5db199SXin Li        Creates a local_result for all directories with a status.log file
284*9c5db199SXin Li        and appends to local_results
285*9c5db199SXin Li        """
286*9c5db199SXin Li        if not os.path.exists(parent_dir) or not os.path.isdir(parent_dir):
287*9c5db199SXin Li            logging.warning('Test directory does not exist: %s' % parent_dir)
288*9c5db199SXin Li            return
289*9c5db199SXin Li
290*9c5db199SXin Li        status_log_file = os.path.join(parent_dir, STATUS_LOG_FILE)
291*9c5db199SXin Li        if os.path.exists(status_log_file):
292*9c5db199SXin Li            self.result_directories.add(parent_dir)
293*9c5db199SXin Li            return
294*9c5db199SXin Li
295*9c5db199SXin Li        for dir_name in os.listdir(parent_dir):
296*9c5db199SXin Li            subdir = os.path.join(parent_dir, dir_name)
297*9c5db199SXin Li            if os.path.isdir(subdir):
298*9c5db199SXin Li                self.enumerate_result_directories(subdir)
299*9c5db199SXin Li
300*9c5db199SXin Li    def set_destination(self, destination):
301*9c5db199SXin Li        self.results_sender.set_destination(destination)
302*9c5db199SXin Li
303*9c5db199SXin Li    def get_fake_moblab_id(self):
304*9c5db199SXin Li        """Get or generate a fake moblab id.
305*9c5db199SXin Li
306*9c5db199SXin Li        Moblab id is the unique id to a moblab device. Since the upload script runs
307*9c5db199SXin Li        from the chroot instead of a moblab device, we need to generate a fake
308*9c5db199SXin Li        moblab id to comply with the CPCon backend. If there is a previously saved
309*9c5db199SXin Li        fake moblab id, read and use it. Otherwise, generate a uuid to fake a moblab
310*9c5db199SXin Li        device, and store it in the same directory as the upload script.
311*9c5db199SXin Li
312*9c5db199SXin Li        Returns:
313*9c5db199SXin Li            A string representing a fake moblab id.
314*9c5db199SXin Li        """
315*9c5db199SXin Li        script_dir = os.path.dirname(__file__)
316*9c5db199SXin Li        fake_moblab_id_path = os.path.join(script_dir, "config",
317*9c5db199SXin Li                                           FAKE_MOBLAB_ID_FILE)
318*9c5db199SXin Li
319*9c5db199SXin Li        # Migrate from prior moblab ID location into config directory if possible
320*9c5db199SXin Li        old_moblab_id_file = os.path.join(script_dir, FAKE_MOBLAB_ID_FILE)
321*9c5db199SXin Li        if os.path.exists(old_moblab_id_file):
322*9c5db199SXin Li            logging.info(
323*9c5db199SXin Li                    'Found an existing moblab ID outside config directory, migrating now'
324*9c5db199SXin Li            )
325*9c5db199SXin Li            os.rename(old_moblab_id_file, fake_moblab_id_path)
326*9c5db199SXin Li        try:
327*9c5db199SXin Li            with open(fake_moblab_id_path, "r") as fake_moblab_id_file:
328*9c5db199SXin Li                fake_moblab_id = str(fake_moblab_id_file.read())[0:32]
329*9c5db199SXin Li                if fake_moblab_id:
330*9c5db199SXin Li                    return fake_moblab_id
331*9c5db199SXin Li        except IOError as e:
332*9c5db199SXin Li            logging.info(
333*9c5db199SXin Li                    'Cannot find a fake moblab id at %s, creating a new one.',
334*9c5db199SXin Li                    fake_moblab_id_path)
335*9c5db199SXin Li        fake_moblab_id = uuid.uuid4().hex
336*9c5db199SXin Li        try:
337*9c5db199SXin Li            with open(fake_moblab_id_path, "w") as fake_moblab_id_file:
338*9c5db199SXin Li                fake_moblab_id_file.write(fake_moblab_id)
339*9c5db199SXin Li        except IOError as e:
340*9c5db199SXin Li            logging.warning('Unable to write the fake moblab id to %s: %s',
341*9c5db199SXin Li                            fake_moblab_id_path, e)
342*9c5db199SXin Li        return fake_moblab_id
343*9c5db199SXin Li
344*9c5db199SXin Li    def overwrite_suite_name(self, suite_name):
345*9c5db199SXin Li        self.suite_name = suite_name
346*9c5db199SXin Li
347*9c5db199SXin Li    def annotate_results_with_bugid(self, bug_id):
348*9c5db199SXin Li        self.bug_id = bug_id
349*9c5db199SXin Li
350*9c5db199SXin Li    def parse_all_results(self, upload_only: bool = False):
351*9c5db199SXin Li        self.results = []
352*9c5db199SXin Li        self.enumerate_all_directories()
353*9c5db199SXin Li
354*9c5db199SXin Li        for result_dir in self.result_directories:
355*9c5db199SXin Li            if self.bug_id is not None:
356*9c5db199SXin Li                self.results_parser.write_bug_id(result_dir, self.bug_id)
357*9c5db199SXin Li            self.results.append(
358*9c5db199SXin Li                    (result_dir,
359*9c5db199SXin Li                     self.results_parser.parse(result_dir,
360*9c5db199SXin Li                                               upload_only,
361*9c5db199SXin Li                                               suite_name=self.suite_name)))
362*9c5db199SXin Li
363*9c5db199SXin Li    def upload_all_results(self, force):
364*9c5db199SXin Li        for result in self.results:
365*9c5db199SXin Li            self.results_sender.upload_result_and_notify(
366*9c5db199SXin Li                    result[0], self.moblab_id, result[1], force)
367*9c5db199SXin Li
368*9c5db199SXin Li
369*9c5db199SXin Liclass FakeTkoDb:
370*9c5db199SXin Li    def find_job(self, tag):
371*9c5db199SXin Li        return None
372*9c5db199SXin Li
373*9c5db199SXin Li    def run_with_retry(self, fn, *args):
374*9c5db199SXin Li        fn(*args)
375*9c5db199SXin Li
376*9c5db199SXin Li
377*9c5db199SXin Liclass ResultsParserClass:
378*9c5db199SXin Li    def __init__(self):
379*9c5db199SXin Li        pass
380*9c5db199SXin Li
381*9c5db199SXin Li    def job_tag(self, job_id, machine):
382*9c5db199SXin Li        return str(job_id) + "-moblab/" + str(machine)
383*9c5db199SXin Li
384*9c5db199SXin Li    def parse(self, path, upload_only: bool, suite_name=""):
385*9c5db199SXin Li        #temporarily assign a fake job id until parsed
386*9c5db199SXin Li        fake_job_id = 1234
387*9c5db199SXin Li        fake_machine = "localhost"
388*9c5db199SXin Li        name = self.job_tag(fake_job_id, fake_machine)
389*9c5db199SXin Li        parse_options = argparse.Namespace(
390*9c5db199SXin Li                **{
391*9c5db199SXin Li                        "suite_report": False,
392*9c5db199SXin Li                        "dry_run": True,
393*9c5db199SXin Li                        "reparse": False,
394*9c5db199SXin Li                        "mail_on_failure": False
395*9c5db199SXin Li                })
396*9c5db199SXin Li        pid_file_manager = pidfile.PidFileManager("parser", path)
397*9c5db199SXin Li        self.print_autotest_git_history(path)
398*9c5db199SXin Li        job = parse_one(FakeTkoDb(), pid_file_manager, name, path,
399*9c5db199SXin Li                        parse_options)
400*9c5db199SXin Li        job.board = job.tests[0].attributes['host-board']
401*9c5db199SXin Li        job_id = int(job.started_time.timestamp() * 1000)
402*9c5db199SXin Li        job.afe_parent_job_id = job_id + 1
403*9c5db199SXin Li        if suite_name == "":
404*9c5db199SXin Li            job.suite = self.parse_suite_name(path)
405*9c5db199SXin Li        else:
406*9c5db199SXin Li            job.suite = suite_name
407*9c5db199SXin Li        job.build_version = self.get_build_version(job.tests)
408*9c5db199SXin Li        name = self.job_tag(job_id, job.machine)
409*9c5db199SXin Li        if not upload_only:
410*9c5db199SXin Li            export_tko_job_to_file(job, name, path + "/job.serialize")
411*9c5db199SXin Li
412*9c5db199SXin Li        # autotest_lib appends additional global logger handlers
413*9c5db199SXin Li        # remove these handlers to avoid affecting logging for the google
414*9c5db199SXin Li        # storage library
415*9c5db199SXin Li        for handler in log.getLogger().handlers:
416*9c5db199SXin Li            log.getLogger().removeHandler(handler)
417*9c5db199SXin Li        return job
418*9c5db199SXin Li
419*9c5db199SXin Li    def print_autotest_git_history(self, path):
420*9c5db199SXin Li        """
421*9c5db199SXin Li        Print the hash of the latest git commit of the autotest directory.
422*9c5db199SXin Li
423*9c5db199SXin Li        Args:
424*9c5db199SXin Li            path: The test directory for non-moblab test results.
425*9c5db199SXin Li        """
426*9c5db199SXin Li        git_hash = subprocess.check_output(shlex.split(GIT_COMMAND),
427*9c5db199SXin Li                                           cwd=AUTOTEST_DIR)
428*9c5db199SXin Li        git_hash_path = os.path.join(path, GIT_HASH_FILE)
429*9c5db199SXin Li        with open(git_hash_path, "w") as git_hash_file:
430*9c5db199SXin Li            git_hash_file.write(git_hash.decode("utf-8"))
431*9c5db199SXin Li
432*9c5db199SXin Li    def parse_suite_name(self, path):
433*9c5db199SXin Li        """Get the suite name from a results directory.
434*9c5db199SXin Li
435*9c5db199SXin Li        If we don't find the suite name in the first ten lines of test_that.DEBUG
436*9c5db199SXin Li        then return None.
437*9c5db199SXin Li
438*9c5db199SXin Li        Args:
439*9c5db199SXin Li            path: The directory specified on the command line.
440*9c5db199SXin Li        """
441*9c5db199SXin Li        path = path.split('/')[:-1]
442*9c5db199SXin Li        path = '/'.join(path)
443*9c5db199SXin Li
444*9c5db199SXin Li        debug_file = os.path.join(path, DEBUG_FILE_PATH)
445*9c5db199SXin Li        if not os.path.exists(debug_file) or not os.path.isfile(debug_file):
446*9c5db199SXin Li            return None
447*9c5db199SXin Li        exp = re.compile(SUITE_NAME_REGEX)
448*9c5db199SXin Li        try:
449*9c5db199SXin Li            with open(debug_file) as f:
450*9c5db199SXin Li                line_count = 0
451*9c5db199SXin Li                for line in f:
452*9c5db199SXin Li                    line_count += 1
453*9c5db199SXin Li                    if line_count > 10:
454*9c5db199SXin Li                        break
455*9c5db199SXin Li                    result = exp.search(line)
456*9c5db199SXin Li                    if not result:
457*9c5db199SXin Li                        continue
458*9c5db199SXin Li                    else:
459*9c5db199SXin Li                        return result.group(1)
460*9c5db199SXin Li        except IOError as e:
461*9c5db199SXin Li            logging.warning('Error trying to read test_that.DEBUG: %s', e)
462*9c5db199SXin Li        return DEFAULT_SUITE_NAME
463*9c5db199SXin Li
464*9c5db199SXin Li    def get_build_version(self, tests):
465*9c5db199SXin Li        release_version_label = "CHROMEOS_RELEASE_VERSION"
466*9c5db199SXin Li        milestone_label = "CHROMEOS_RELEASE_CHROME_MILESTONE"
467*9c5db199SXin Li        for test in tests:
468*9c5db199SXin Li            if not test.subdir:
469*9c5db199SXin Li                continue
470*9c5db199SXin Li
471*9c5db199SXin Li            release = None
472*9c5db199SXin Li            milestone = None
473*9c5db199SXin Li            if release_version_label in test.attributes:
474*9c5db199SXin Li                release = test.attributes[release_version_label]
475*9c5db199SXin Li            if milestone_label in test.attributes:
476*9c5db199SXin Li                milestone = test.attributes[milestone_label]
477*9c5db199SXin Li            if release and milestone:
478*9c5db199SXin Li                return "R%s-%s" % (milestone, release)
479*9c5db199SXin Li
480*9c5db199SXin Li        return ""
481*9c5db199SXin Li
482*9c5db199SXin Li    def valid_bug_id(self, v):
483*9c5db199SXin Li        """Check if user input bug id is in valid format.
484*9c5db199SXin Li
485*9c5db199SXin Li        Args:
486*9c5db199SXin Li            v: User input bug id in string.
487*9c5db199SXin Li        Returns:
488*9c5db199SXin Li            An int representing the bug id.
489*9c5db199SXin Li        Raises:
490*9c5db199SXin Li            argparse.ArgumentTypeError: if user input bug id has wrong format.
491*9c5db199SXin Li        """
492*9c5db199SXin Li        try:
493*9c5db199SXin Li            bug_id = int(v)
494*9c5db199SXin Li        except ValueError as e:
495*9c5db199SXin Li            raise argparse.ArgumentTypeError(
496*9c5db199SXin Li                    "Bug id %s is not a positive integer: "
497*9c5db199SXin Li                    "%s" % (v, e))
498*9c5db199SXin Li        if bug_id <= 0:
499*9c5db199SXin Li            raise argparse.ArgumentTypeError(
500*9c5db199SXin Li                    "Bug id %s is not a positive integer" % v)
501*9c5db199SXin Li        return bug_id
502*9c5db199SXin Li
503*9c5db199SXin Li    def write_bug_id(self, test_dir, bug_id):
504*9c5db199SXin Li        """
505*9c5db199SXin Li            Write the bug id to the test results.
506*9c5db199SXin Li
507*9c5db199SXin Li        Args:
508*9c5db199SXin Li            test_dir: The test directory for non-moblab test results.
509*9c5db199SXin Li            bug_id: The bug id to write to the test results.
510*9c5db199SXin Li        Returns:
511*9c5db199SXin Li            A boolean. True if the bug id is written successfully or is the same as
512*9c5db199SXin Li            the old bug id already in test results; False if failed to write the
513*9c5db199SXin Li            bug id, or if the user decides not to overwrite the old bug id already
514*9c5db199SXin Li            in test results.
515*9c5db199SXin Li        """
516*9c5db199SXin Li        old_bug_id = None
517*9c5db199SXin Li        new_keyval = list()
518*9c5db199SXin Li
519*9c5db199SXin Li        keyval_file = os.path.join(test_dir, KEYVAL_FILE)
520*9c5db199SXin Li        try:
521*9c5db199SXin Li            with open(keyval_file, 'r') as keyval_raw:
522*9c5db199SXin Li                for line in keyval_raw.readlines():
523*9c5db199SXin Li                    match = re.match(r'bug_id=(\d+)', line)
524*9c5db199SXin Li                    if match:
525*9c5db199SXin Li                        old_bug_id = self.valid_bug_id(match.group(1))
526*9c5db199SXin Li                    else:
527*9c5db199SXin Li                        new_keyval.append(line)
528*9c5db199SXin Li        except IOError as e:
529*9c5db199SXin Li            logging.error(
530*9c5db199SXin Li                    'Cannot read keyval file from %s, skip writing the bug '
531*9c5db199SXin Li                    'id %s: %s', test_dir, bug_id, e)
532*9c5db199SXin Li            return False
533*9c5db199SXin Li
534*9c5db199SXin Li        if old_bug_id:
535*9c5db199SXin Li            if old_bug_id == bug_id:
536*9c5db199SXin Li                return True
537*9c5db199SXin Li            overwrite_bug_id = _confirm_option(
538*9c5db199SXin Li                    'Would you like to overwrite bug id '
539*9c5db199SXin Li                    '%s with new bug id %s?' % (old_bug_id, bug_id))
540*9c5db199SXin Li            if not overwrite_bug_id:
541*9c5db199SXin Li                return False
542*9c5db199SXin Li
543*9c5db199SXin Li        new_keyval.append('bug_id=%s' % bug_id)
544*9c5db199SXin Li        new_keyval_file = os.path.join(test_dir, NEW_KEYVAL_FILE)
545*9c5db199SXin Li        try:
546*9c5db199SXin Li            with open(new_keyval_file, 'w') as new_keyval_raw:
547*9c5db199SXin Li                for line in new_keyval:
548*9c5db199SXin Li                    new_keyval_raw.write(line)
549*9c5db199SXin Li                new_keyval_raw.write('\n')
550*9c5db199SXin Li            shutil.move(new_keyval_file, keyval_file)
551*9c5db199SXin Li            return True
552*9c5db199SXin Li        except Exception as e:
553*9c5db199SXin Li            logging.error(
554*9c5db199SXin Li                    'Cannot write bug id to keyval file in %s, skip writing '
555*9c5db199SXin Li                    'the bug id %s: %s', test_dir, bug_id, e)
556*9c5db199SXin Li            return False
557*9c5db199SXin Li
558*9c5db199SXin Li
559*9c5db199SXin LiResultsParser = ResultsParserClass()
560*9c5db199SXin Li_valid_bug_id = functools.partial(ResultsParserClass.valid_bug_id,
561*9c5db199SXin Li                                  ResultsParser)
562*9c5db199SXin Li
563*9c5db199SXin Li
564*9c5db199SXin Liclass ResultsSenderClass:
565*9c5db199SXin Li    def __init__(self):
566*9c5db199SXin Li        self.gcs_bucket = ""
567*9c5db199SXin Li
568*9c5db199SXin Li    def set_destination(self, destination):
569*9c5db199SXin Li        self.gcs_bucket = destination
570*9c5db199SXin Li
571*9c5db199SXin Li    def upload_result_and_notify(self, test_dir, moblab_id, job, force):
572*9c5db199SXin Li        job_id = str(int(job.started_time.timestamp() * 1000))
573*9c5db199SXin Li        if self.uploaded(test_dir) and not force:
574*9c5db199SXin Li            return
575*9c5db199SXin Li        self.upload_result(test_dir, moblab_id, job_id, job.machine)
576*9c5db199SXin Li        self.send_pubsub_message(test_dir, moblab_id, job_id)
577*9c5db199SXin Li
578*9c5db199SXin Li    def upload_batch_files(self, gs_path, test_dir, files):
579*9c5db199SXin Li        for file in files:
580*9c5db199SXin Li            if not os.path.isfile(file):
581*9c5db199SXin Li                continue
582*9c5db199SXin Li            gs_client_bucket = storage.Client().bucket(self.gcs_bucket)
583*9c5db199SXin Li            # remove trailing slash to ensure dest_file path gets created properly
584*9c5db199SXin Li            test_dir = test_dir.rstrip('/')
585*9c5db199SXin Li            dest_file = gs_path + file.replace(test_dir, "", 1)
586*9c5db199SXin Li            logging.info("uploading file: %s", dest_file)
587*9c5db199SXin Li            blob = gs_client_bucket.blob(dest_file)
588*9c5db199SXin Li            blob.upload_from_filename(file)
589*9c5db199SXin Li
590*9c5db199SXin Li    def upload_result(self, test_dir, moblab_id, job_id, hostname):
591*9c5db199SXin Li        """
592*9c5db199SXin Li            Upload the test directory with job.serialize to GCS bucket.
593*9c5db199SXin Li
594*9c5db199SXin Li        Args:
595*9c5db199SXin Li            args: A list of input arguments.
596*9c5db199SXin Li            test_dir: The test directory for non-moblab test results.
597*9c5db199SXin Li            job_keyval: The key-value object of the job.
598*9c5db199SXin Li            moblab_id: A string that represents the unique id of a moblab device.
599*9c5db199SXin Li            job_id: A job id.
600*9c5db199SXin Li        """
601*9c5db199SXin Li        upload_status_file = os.path.join(test_dir, UPLOADED_STATUS_FILE)
602*9c5db199SXin Li        with open(upload_status_file, "w") as upload_status:
603*9c5db199SXin Li            upload_status.write("UPLOADING")
604*9c5db199SXin Li
605*9c5db199SXin Li        fake_moblab_id = moblab_id
606*9c5db199SXin Li        fake_moblab_install_id = moblab_id
607*9c5db199SXin Li
608*9c5db199SXin Li        gcs_bucket_path = os.path.join("results", fake_moblab_id,
609*9c5db199SXin Li                                       fake_moblab_install_id,
610*9c5db199SXin Li                                       "%s-moblab" % job_id, hostname)
611*9c5db199SXin Li
612*9c5db199SXin Li        try:
613*9c5db199SXin Li            logging.info(
614*9c5db199SXin Li                    "Start to upload test directory: %s to GCS bucket path: %s",
615*9c5db199SXin Li                    test_dir, gcs_bucket_path)
616*9c5db199SXin Li            with open(upload_status_file, "w") as upload_status:
617*9c5db199SXin Li                upload_status.write("UPLOADED")
618*9c5db199SXin Li
619*9c5db199SXin Li            files_to_upload = glob.glob(test_dir + "/**", recursive=True)
620*9c5db199SXin Li            batch_size = 8
621*9c5db199SXin Li            with multiprocessing.Pool(4) as p:
622*9c5db199SXin Li                files_to_upload_batch = [
623*9c5db199SXin Li                        files_to_upload[i:i + batch_size]
624*9c5db199SXin Li                        for i in range(0, len(files_to_upload), batch_size)
625*9c5db199SXin Li                ]
626*9c5db199SXin Li                p.map(
627*9c5db199SXin Li                        functools.partial(
628*9c5db199SXin Li                                ResultsSenderClass.upload_batch_files, self,
629*9c5db199SXin Li                                gcs_bucket_path, test_dir),
630*9c5db199SXin Li                        files_to_upload_batch)
631*9c5db199SXin Li
632*9c5db199SXin Li            logging.info(
633*9c5db199SXin Li                    "Successfully uploaded test directory: %s to GCS bucket path: %s",
634*9c5db199SXin Li                    test_dir, gcs_bucket_path)
635*9c5db199SXin Li        except Exception as e:
636*9c5db199SXin Li            with open(upload_status_file, "w") as upload_status:
637*9c5db199SXin Li                upload_status.write("UPLOAD_FAILED")
638*9c5db199SXin Li            raise Exception(
639*9c5db199SXin Li                    "Failed to upload test directory: %s to GCS bucket "
640*9c5db199SXin Li                    "path: %s for the error: %s" %
641*9c5db199SXin Li                    (test_dir, gcs_bucket_path, e))
642*9c5db199SXin Li
643*9c5db199SXin Li    def send_pubsub_message(self, test_dir, moblab_id, job_id):
644*9c5db199SXin Li        """
645*9c5db199SXin Li            Send pubsub messages to trigger CPCon pipeline to process non-moblab
646*9c5db199SXin Li            test results in the specific GCS bucket path.
647*9c5db199SXin Li
648*9c5db199SXin Li        Args:
649*9c5db199SXin Li            bucket: The GCS bucket.
650*9c5db199SXin Li            moblab_id: A moblab id.
651*9c5db199SXin Li            job_id: A job id.
652*9c5db199SXin Li        """
653*9c5db199SXin Li        moblab_install_id = moblab_id
654*9c5db199SXin Li        console_client = pubsub_client.PubSubBasedClient()
655*9c5db199SXin Li        gsuri = "gs://%s/results/%s/%s/%s-moblab" % (
656*9c5db199SXin Li                self.gcs_bucket, moblab_id, moblab_install_id, job_id)
657*9c5db199SXin Li
658*9c5db199SXin Li        try:
659*9c5db199SXin Li            logging.info("Start to send the pubsub message to GCS path: %s",
660*9c5db199SXin Li                         gsuri)
661*9c5db199SXin Li            message_id = \
662*9c5db199SXin Li                console_client.send_test_job_offloaded_message(gsuri,
663*9c5db199SXin Li                                                            moblab_id,
664*9c5db199SXin Li                                                            moblab_install_id)
665*9c5db199SXin Li            upload_status_file = os.path.join(test_dir, UPLOADED_STATUS_FILE)
666*9c5db199SXin Li            with open(upload_status_file, "w") as upload_status:
667*9c5db199SXin Li                upload_status.write(STATUS_GOOD)
668*9c5db199SXin Li
669*9c5db199SXin Li            logging.info(
670*9c5db199SXin Li                    "Successfully sent the pubsub message with message id: %s to GCS "
671*9c5db199SXin Li                    "path: %s", message_id[0], gsuri)
672*9c5db199SXin Li        except Exception as e:
673*9c5db199SXin Li            raise Exception(
674*9c5db199SXin Li                    "Failed to send the pubsub message with moblab id: %s "
675*9c5db199SXin Li                    "and job id: %s to GCS path: %s for the error: %s" %
676*9c5db199SXin Li                    (moblab_id, job_id, gsuri, e))
677*9c5db199SXin Li
678*9c5db199SXin Li    def uploaded(self, test_dir):
679*9c5db199SXin Li        """
680*9c5db199SXin Li        Checks if the message for the uploaded bucket has been sent.
681*9c5db199SXin Li
682*9c5db199SXin Li        Args:
683*9c5db199SXin Li            test_dir: The test directory for non-moblab test results.
684*9c5db199SXin Li        """
685*9c5db199SXin Li        upload_status_file = os.path.join(test_dir, UPLOADED_STATUS_FILE)
686*9c5db199SXin Li        if not os.path.exists(upload_status_file):
687*9c5db199SXin Li            logging.debug("The upload status file %s does not exist.",
688*9c5db199SXin Li                          upload_status_file)
689*9c5db199SXin Li            return False
690*9c5db199SXin Li
691*9c5db199SXin Li        with open(upload_status_file, "r") as upload_status:
692*9c5db199SXin Li            if upload_status.read() == STATUS_GOOD:
693*9c5db199SXin Li                logging.warn(
694*9c5db199SXin Li                        "The test directory: %s status has already been "
695*9c5db199SXin Li                        "sent to CPCon and the .upload_status file has "
696*9c5db199SXin Li                        "been set to PUBSUB_SENT.", test_dir)
697*9c5db199SXin Li                return True
698*9c5db199SXin Li            else:
699*9c5db199SXin Li                logging.debug("The pubsub message was not successful")
700*9c5db199SXin Li        return False
701*9c5db199SXin Li
702*9c5db199SXin Li
703*9c5db199SXin LiResultsSender = ResultsSenderClass()
704*9c5db199SXin Li
705*9c5db199SXin Li
706*9c5db199SXin Lidef main(args):
707*9c5db199SXin Li    parsed_args = parse_arguments(args)
708*9c5db199SXin Li
709*9c5db199SXin Li    fmt = log.Formatter('%(asctime)s :: %(levelname)-8s :: %(message)s')
710*9c5db199SXin Li    logging.propagate = False
711*9c5db199SXin Li
712*9c5db199SXin Li    log_level = log.INFO
713*9c5db199SXin Li    if parsed_args.verbose:
714*9c5db199SXin Li        log_level = log.DEBUG
715*9c5db199SXin Li    if not parsed_args.quiet:
716*9c5db199SXin Li        stream_handler = log.StreamHandler(sys.stdout)
717*9c5db199SXin Li        stream_handler.setFormatter(fmt)
718*9c5db199SXin Li        stream_handler.setLevel(log_level)
719*9c5db199SXin Li        logging.addHandler(stream_handler)
720*9c5db199SXin Li
721*9c5db199SXin Li    logging.info("logging to %s", parsed_args.logfile)
722*9c5db199SXin Li    file_handler = log.FileHandler(parsed_args.logfile, mode='w')
723*9c5db199SXin Li    file_handler.setFormatter(fmt)
724*9c5db199SXin Li    file_handler.setLevel(log.DEBUG)
725*9c5db199SXin Li    logging.addHandler(file_handler)
726*9c5db199SXin Li
727*9c5db199SXin Li    if parsed_args.subcommand == "config":
728*9c5db199SXin Li        _configure_environment(parsed_args)
729*9c5db199SXin Li        return
730*9c5db199SXin Li
731*9c5db199SXin Li    persistent_settings = _load_config()
732*9c5db199SXin Li
733*9c5db199SXin Li    results_manager = ResultsManager(ResultsParser, ResultsSender)
734*9c5db199SXin Li    results_manager.set_destination(persistent_settings.bucket)
735*9c5db199SXin Li    results_manager.new_directory(parsed_args.directory)
736*9c5db199SXin Li
737*9c5db199SXin Li    if parsed_args.bug:
738*9c5db199SXin Li        results_manager.annotate_results_with_bugid(parsed_args.bug)
739*9c5db199SXin Li    if parsed_args.suite:
740*9c5db199SXin Li        results_manager.overwrite_suite_name(parsed_args.suite)
741*9c5db199SXin Li    if parsed_args.parse_only:
742*9c5db199SXin Li        results_manager.parse_all_results()
743*9c5db199SXin Li    elif parsed_args.upload_only:
744*9c5db199SXin Li        results_manager.parse_all_results(upload_only=True)
745*9c5db199SXin Li        results_manager.upload_all_results(force=parsed_args.force)
746*9c5db199SXin Li    else:
747*9c5db199SXin Li        results_manager.parse_all_results()
748*9c5db199SXin Li        results_manager.upload_all_results(force=parsed_args.force)
749*9c5db199SXin Li
750*9c5db199SXin Li
751*9c5db199SXin Liif __name__ == "__main__":
752*9c5db199SXin Li    try:
753*9c5db199SXin Li        main(sys.argv[1:])
754*9c5db199SXin Li    except KeyboardInterrupt:
755*9c5db199SXin Li        sys.exit(0)
756