xref: /aosp_15_r20/external/toolchain-utils/cros_utils/bugs.py (revision 760c253c1ed00ce9abd48f8546f08516e57485fe)
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