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