1*760c253cSXin Li#!/usr/bin/env python3 2*760c253cSXin Li# Copyright 2021 The ChromiumOS Authors 3*760c253cSXin Li# Use of this source code is governed by a BSD-style license that can be 4*760c253cSXin Li# found in the LICENSE file. 5*760c253cSXin Li"""Utilities to file bugs.""" 6*760c253cSXin Li 7*760c253cSXin Liimport datetime 8*760c253cSXin Liimport enum 9*760c253cSXin Liimport json 10*760c253cSXin Liimport os 11*760c253cSXin Liimport threading 12*760c253cSXin Lifrom typing import Any, Dict, List, Optional, Union 13*760c253cSXin Li 14*760c253cSXin Li 15*760c253cSXin LiX20_PATH = "/google/data/rw/teams/c-compiler-chrome/prod_bugs" 16*760c253cSXin Li 17*760c253cSXin Li# List of 'well-known' bug numbers to tag as parents. 18*760c253cSXin LiRUST_MAINTENANCE_METABUG = 322195383 19*760c253cSXin LiRUST_SECURITY_METABUG = 322195192 20*760c253cSXin Li 21*760c253cSXin Li 22*760c253cSXin Li# These constants are sourced from 23*760c253cSXin Li# //google3/googleclient/chrome/chromeos_toolchain/bug_manager/bugs.go 24*760c253cSXin Liclass WellKnownComponents(enum.IntEnum): 25*760c253cSXin Li """A listing of "well-known" components recognized by our infra.""" 26*760c253cSXin Li 27*760c253cSXin Li CrOSToolchainPublic = -1 28*760c253cSXin Li CrOSToolchainPrivate = -2 29*760c253cSXin Li AndroidRustToolchain = -3 30*760c253cSXin Li 31*760c253cSXin Li 32*760c253cSXin Liclass _FileNameGenerator: 33*760c253cSXin Li """Generates unique file names. This container is thread-safe. 34*760c253cSXin Li 35*760c253cSXin Li The names generated have the following properties: 36*760c253cSXin Li - successive, sequenced calls to `get_json_file_name()` will produce 37*760c253cSXin Li names that sort later in lists over time (e.g., 38*760c253cSXin Li [generator.generate_json_file_name() for _ in range(10)] will be in 39*760c253cSXin Li sorted order). 40*760c253cSXin Li - file names cannot collide with file names generated on the same 41*760c253cSXin Li machine (ignoring machines with unreasonable PID reuse). 42*760c253cSXin Li - file names are incredibly unlikely to collide when generated on 43*760c253cSXin Li multiple machines, as they have 8 bytes of entropy in them. 44*760c253cSXin Li """ 45*760c253cSXin Li 46*760c253cSXin Li _RANDOM_BYTES = 8 47*760c253cSXin Li _MAX_OS_ENTROPY_VALUE = 1 << _RANDOM_BYTES * 8 48*760c253cSXin Li # The intent of this is "the maximum possible size of our entropy string, 49*760c253cSXin Li # so we can zfill properly below." Double the value the OS hands us, since 50*760c253cSXin Li # we add to it in `generate_json_file_name`. 51*760c253cSXin Li _ENTROPY_STR_SIZE = len(str(2 * _MAX_OS_ENTROPY_VALUE)) 52*760c253cSXin Li 53*760c253cSXin Li def __init__(self): 54*760c253cSXin Li self._lock = threading.Lock() 55*760c253cSXin Li self._entropy = int.from_bytes( 56*760c253cSXin Li os.getrandom(self._RANDOM_BYTES), byteorder="little", signed=False 57*760c253cSXin Li ) 58*760c253cSXin Li 59*760c253cSXin Li def generate_json_file_name(self, now: datetime.datetime): 60*760c253cSXin Li with self._lock: 61*760c253cSXin Li my_entropy = self._entropy 62*760c253cSXin Li self._entropy += 1 63*760c253cSXin Li 64*760c253cSXin Li now_str = now.isoformat("T", "seconds") + "Z" 65*760c253cSXin Li entropy_str = str(my_entropy).zfill(self._ENTROPY_STR_SIZE) 66*760c253cSXin Li pid = os.getpid() 67*760c253cSXin Li return f"{now_str}_{entropy_str}_{pid}.json" 68*760c253cSXin Li 69*760c253cSXin Li 70*760c253cSXin Li_GLOBAL_NAME_GENERATOR = _FileNameGenerator() 71*760c253cSXin Li 72*760c253cSXin Li 73*760c253cSXin Lidef _WriteBugJSONFile( 74*760c253cSXin Li object_type: str, 75*760c253cSXin Li json_object: Dict[str, Any], 76*760c253cSXin Li directory: Optional[Union[os.PathLike, str]], 77*760c253cSXin Li): 78*760c253cSXin Li """Writes a JSON file to `directory` with the given bug-ish object. 79*760c253cSXin Li 80*760c253cSXin Li Args: 81*760c253cSXin Li object_type: name of the object we're writing. 82*760c253cSXin Li json_object: object to write. 83*760c253cSXin Li directory: the directory to write to. Uses X20_PATH if None. 84*760c253cSXin Li """ 85*760c253cSXin Li final_object = { 86*760c253cSXin Li "type": object_type, 87*760c253cSXin Li "value": json_object, 88*760c253cSXin Li } 89*760c253cSXin Li 90*760c253cSXin Li if directory is None: 91*760c253cSXin Li directory = X20_PATH 92*760c253cSXin Li 93*760c253cSXin Li now = datetime.datetime.now(tz=datetime.timezone.utc) 94*760c253cSXin Li file_path = os.path.join( 95*760c253cSXin Li directory, _GLOBAL_NAME_GENERATOR.generate_json_file_name(now) 96*760c253cSXin Li ) 97*760c253cSXin Li temp_path = file_path + ".in_progress" 98*760c253cSXin Li try: 99*760c253cSXin Li with open(temp_path, "w", encoding="utf-8") as f: 100*760c253cSXin Li json.dump(final_object, f) 101*760c253cSXin Li os.rename(temp_path, file_path) 102*760c253cSXin Li except: 103*760c253cSXin Li os.remove(temp_path) 104*760c253cSXin Li raise 105*760c253cSXin Li return file_path 106*760c253cSXin Li 107*760c253cSXin Li 108*760c253cSXin Lidef AppendToExistingBug( 109*760c253cSXin Li bug_id: int, body: str, directory: Optional[os.PathLike] = None 110*760c253cSXin Li): 111*760c253cSXin Li """Sends a reply to an existing bug.""" 112*760c253cSXin Li _WriteBugJSONFile( 113*760c253cSXin Li "AppendToExistingBugRequest", 114*760c253cSXin Li { 115*760c253cSXin Li "body": body, 116*760c253cSXin Li "bug_id": bug_id, 117*760c253cSXin Li }, 118*760c253cSXin Li directory, 119*760c253cSXin Li ) 120*760c253cSXin Li 121*760c253cSXin Li 122*760c253cSXin Lidef CreateNewBug( 123*760c253cSXin Li component_id: int, 124*760c253cSXin Li title: str, 125*760c253cSXin Li body: str, 126*760c253cSXin Li assignee: Optional[str] = None, 127*760c253cSXin Li cc: Optional[List[str]] = None, 128*760c253cSXin Li directory: Optional[os.PathLike] = None, 129*760c253cSXin Li parent_bug: int = 0, 130*760c253cSXin Li): 131*760c253cSXin Li """Sends a request to create a new bug. 132*760c253cSXin Li 133*760c253cSXin Li Args: 134*760c253cSXin Li component_id: The component ID to add. Anything from WellKnownComponents 135*760c253cSXin Li also works. 136*760c253cSXin Li title: Title of the bug. Must be nonempty. 137*760c253cSXin Li body: Body of the bug. Must be nonempty. 138*760c253cSXin Li assignee: Assignee of the bug. Must be either an email address, or a 139*760c253cSXin Li "well-known" assignee (detective, mage). 140*760c253cSXin Li cc: A list of emails to add to the CC list. Must either be an email 141*760c253cSXin Li address, or a "well-known" individual (detective, mage). 142*760c253cSXin Li directory: The directory to write the report to. Defaults to our x20 143*760c253cSXin Li bugs directory. 144*760c253cSXin Li parent_bug: The parent bug number for this bug. If none should be 145*760c253cSXin Li specified, pass the value 0. 146*760c253cSXin Li """ 147*760c253cSXin Li obj = { 148*760c253cSXin Li "component_id": component_id, 149*760c253cSXin Li "subject": title, 150*760c253cSXin Li "body": body, 151*760c253cSXin Li } 152*760c253cSXin Li 153*760c253cSXin Li if assignee: 154*760c253cSXin Li obj["assignee"] = assignee 155*760c253cSXin Li 156*760c253cSXin Li if cc: 157*760c253cSXin Li obj["cc"] = cc 158*760c253cSXin Li 159*760c253cSXin Li if parent_bug: 160*760c253cSXin Li obj["parent_bug"] = parent_bug 161*760c253cSXin Li 162*760c253cSXin Li _WriteBugJSONFile("FileNewBugRequest", obj, directory) 163*760c253cSXin Li 164*760c253cSXin Li 165*760c253cSXin Lidef SendCronjobLog( 166*760c253cSXin Li cronjob_name: str, 167*760c253cSXin Li failed: bool, 168*760c253cSXin Li message: str, 169*760c253cSXin Li turndown_time_hours: int = 0, 170*760c253cSXin Li directory: Optional[os.PathLike] = None, 171*760c253cSXin Li parent_bug: int = 0, 172*760c253cSXin Li): 173*760c253cSXin Li """Sends the record of a cronjob to our bug infra. 174*760c253cSXin Li 175*760c253cSXin Li Args: 176*760c253cSXin Li cronjob_name: The name of the cronjob. Expected to remain consistent 177*760c253cSXin Li over time. 178*760c253cSXin Li failed: Whether the job failed or not. 179*760c253cSXin Li message: Any seemingly relevant context. This is pasted verbatim in a 180*760c253cSXin Li bug, if the cronjob infra deems it worthy. 181*760c253cSXin Li turndown_time_hours: If nonzero, this cronjob will be considered turned 182*760c253cSXin Li down if more than `turndown_time_hours` pass without a report of 183*760c253cSXin Li success or failure. If zero, this job will not automatically be 184*760c253cSXin Li turned down. 185*760c253cSXin Li directory: The directory to write the report to. Defaults to our x20 186*760c253cSXin Li bugs directory. 187*760c253cSXin Li parent_bug: The parent bug number for the bug filed for this cronjob, 188*760c253cSXin Li if any. If none should be specified, pass the value 0. 189*760c253cSXin Li """ 190*760c253cSXin Li json_object = { 191*760c253cSXin Li "name": cronjob_name, 192*760c253cSXin Li "message": message, 193*760c253cSXin Li "failed": failed, 194*760c253cSXin Li } 195*760c253cSXin Li 196*760c253cSXin Li if turndown_time_hours: 197*760c253cSXin Li json_object["cronjob_turndown_time_hours"] = turndown_time_hours 198*760c253cSXin Li 199*760c253cSXin Li if parent_bug: 200*760c253cSXin Li json_object["parent_bug"] = parent_bug 201*760c253cSXin Li 202*760c253cSXin Li _WriteBugJSONFile("CronjobUpdate", json_object, directory) 203