|
| 1 | +"""JIRA Ticket Creator.""" |
| 2 | + |
| 3 | +import requests |
| 4 | +from requests.auth import HTTPBasicAuth |
| 5 | +import json |
| 6 | +import webbrowser |
| 7 | +import argparse |
| 8 | +from typing import List, Tuple |
| 9 | +from abr_testing.data_collection import read_robot_logs, abr_google_drive, get_run_logs |
| 10 | + |
| 11 | + |
| 12 | +def get_error_runs_from_robot(ip: str) -> List[str]: |
| 13 | + """Get runs that have errors from robot.""" |
| 14 | + error_run_ids = [] |
| 15 | + response = requests.get( |
| 16 | + f"http://{ip}:31950/runs", headers={"opentrons-version": "3"} |
| 17 | + ) |
| 18 | + run_data = response.json() |
| 19 | + run_list = run_data["data"] |
| 20 | + for run in run_list: |
| 21 | + run_id = run["id"] |
| 22 | + num_of_errors = len(run["errors"]) |
| 23 | + if not run["current"] and num_of_errors > 0: |
| 24 | + error_run_ids.append(run_id) |
| 25 | + return error_run_ids |
| 26 | + |
| 27 | + |
| 28 | +def get_error_info_from_robot( |
| 29 | + ip: str, one_run: str, storage_directory: str |
| 30 | +) -> Tuple[str, str, str, List[str], str, str]: |
| 31 | + """Get error information from robot to fill out ticket.""" |
| 32 | + description = dict() |
| 33 | + # get run information |
| 34 | + results = get_run_logs.get_run_data(one_run, ip) |
| 35 | + # save run information to local directory as .json file |
| 36 | + saved_file_path = read_robot_logs.save_run_log_to_json( |
| 37 | + ip, results, storage_directory |
| 38 | + ) |
| 39 | + |
| 40 | + # Error Printout |
| 41 | + ( |
| 42 | + num_of_errors, |
| 43 | + error_type, |
| 44 | + error_code, |
| 45 | + error_instrument, |
| 46 | + error_level, |
| 47 | + ) = read_robot_logs.get_error_info(results) |
| 48 | + # JIRA Ticket Fields |
| 49 | + failure_level = "Level " + str(error_level) + " Failure" |
| 50 | + components = [failure_level, "Flex-RABR"] |
| 51 | + affects_version = results["API_Version"] |
| 52 | + parent = results.get("robot_name", "") |
| 53 | + print(parent) |
| 54 | + summary = parent + "_" + str(one_run) + "_" + str(error_code) + "_" + error_type |
| 55 | + # Description of error |
| 56 | + description["protocol_name"] = results["protocol"]["metadata"].get( |
| 57 | + "protocolName", "" |
| 58 | + ) |
| 59 | + description["error"] = " ".join([error_code, error_type, error_instrument]) |
| 60 | + description["protocol_step"] = list(results["commands"])[-1] |
| 61 | + description["right_mount"] = results.get("right", "No attachment") |
| 62 | + description["left_mount"] = results.get("left", "No attachment") |
| 63 | + description["gripper"] = results.get("extension", "No attachment") |
| 64 | + all_modules = abr_google_drive.get_modules(results) |
| 65 | + whole_description = {**description, **all_modules} |
| 66 | + whole_description_str = ( |
| 67 | + "{" |
| 68 | + + "\n".join("{!r}: {!r},".format(k, v) for k, v in whole_description.items()) |
| 69 | + + "}" |
| 70 | + ) |
| 71 | + |
| 72 | + return ( |
| 73 | + summary, |
| 74 | + parent, |
| 75 | + affects_version, |
| 76 | + components, |
| 77 | + whole_description_str, |
| 78 | + saved_file_path, |
| 79 | + ) |
| 80 | + |
| 81 | + |
| 82 | +class JiraTicket: |
| 83 | + """Connects to JIRA ticket site.""" |
| 84 | + |
| 85 | + def __init__(self, url: str, api_token: str, email: str) -> None: |
| 86 | + """Connect to jira.""" |
| 87 | + self.url = url |
| 88 | + self.api_token = api_token |
| 89 | + self.email = email |
| 90 | + self.auth = HTTPBasicAuth(email, api_token) |
| 91 | + self.headers = { |
| 92 | + "Accept": "application/json", |
| 93 | + "Content-Type": "application/json", |
| 94 | + } |
| 95 | + |
| 96 | + def issues_on_board(self, board_id: str) -> List[str]: |
| 97 | + """Print Issues on board.""" |
| 98 | + response = requests.get( |
| 99 | + f"{self.url}/rest/agile/1.0/board/{board_id}/issue", |
| 100 | + headers=self.headers, |
| 101 | + auth=self.auth, |
| 102 | + ) |
| 103 | + response.raise_for_status() |
| 104 | + try: |
| 105 | + board_data = response.json() |
| 106 | + all_issues = board_data["issues"] |
| 107 | + except json.JSONDecodeError as e: |
| 108 | + print("Error decoding json: ", e) |
| 109 | + issue_ids = [] |
| 110 | + for i in all_issues: |
| 111 | + issue_id = i.get("id") |
| 112 | + issue_ids.append(issue_id) |
| 113 | + return issue_ids |
| 114 | + |
| 115 | + def open_issue(self, issue_key: str) -> None: |
| 116 | + """Open issue on web browser.""" |
| 117 | + url = f"{self.url}/browse/{issue_key}" |
| 118 | + webbrowser.open(url) |
| 119 | + |
| 120 | + def create_ticket( |
| 121 | + self, |
| 122 | + summary: str, |
| 123 | + description: str, |
| 124 | + project_key: str, |
| 125 | + reporter_id: str, |
| 126 | + issue_type: str, |
| 127 | + priority: str, |
| 128 | + components: list, |
| 129 | + affects_versions: str, |
| 130 | + robot: str, |
| 131 | + ) -> Tuple[str, str]: |
| 132 | + """Create ticket.""" |
| 133 | + data = { |
| 134 | + "fields": { |
| 135 | + "project": {"id": "10273", "key": project_key}, |
| 136 | + "issuetype": {"name": issue_type}, |
| 137 | + "summary": summary, |
| 138 | + "reporter": {"id": reporter_id}, |
| 139 | + "parent": {"key": robot}, |
| 140 | + "priority": {"name": priority}, |
| 141 | + "components": [{"name": component} for component in components], |
| 142 | + "versions": [{"name": affects_versions}], |
| 143 | + "description": { |
| 144 | + "content": [ |
| 145 | + { |
| 146 | + "content": [{"text": description, "type": "text"}], |
| 147 | + "type": "paragraph", |
| 148 | + } |
| 149 | + ], |
| 150 | + "type": "doc", |
| 151 | + "version": 1, |
| 152 | + } |
| 153 | + # Include other required fields as needed |
| 154 | + } |
| 155 | + } |
| 156 | + try: |
| 157 | + response = requests.post( |
| 158 | + f"{self.url}/rest/api/3/issue/", |
| 159 | + headers=self.headers, |
| 160 | + auth=self.auth, |
| 161 | + json=data, |
| 162 | + ) |
| 163 | + response.raise_for_status() |
| 164 | + response_str = str(response.content) |
| 165 | + issue_url = response.json().get("self") |
| 166 | + issue_key = response.json().get("key") |
| 167 | + if issue_key is None: |
| 168 | + print("Error: Could not create issue. No key returned.") |
| 169 | + except requests.exceptions.HTTPError: |
| 170 | + print(f"HTTP error occurred. Response content: {response_str}") |
| 171 | + except json.JSONDecodeError: |
| 172 | + print(f"JSON decoding error occurred. Response content: {response_str}") |
| 173 | + return issue_url, issue_key |
| 174 | + |
| 175 | + def post_attachment_to_ticket(self, issue_id: str, attachment_path: str) -> None: |
| 176 | + """Adds attachments to ticket.""" |
| 177 | + # TODO: Ensure that file is actually uploaded. |
| 178 | + file = {"file": open(attachment_path, "rb")} |
| 179 | + JSON_headers = {"Accept": "application/json"} |
| 180 | + try: |
| 181 | + response = requests.post( |
| 182 | + f"{self.url}/rest/api/3/issue/{issue_id}/attachments", |
| 183 | + headers=JSON_headers, |
| 184 | + auth=self.auth, |
| 185 | + files=file, |
| 186 | + ) |
| 187 | + print(response) |
| 188 | + except json.JSONDecodeError: |
| 189 | + error_message = str(response.content) |
| 190 | + print(f"JSON decoding error occurred. Response content: {error_message}.") |
| 191 | + |
| 192 | + |
| 193 | +if __name__ == "__main__": |
| 194 | + """Create ticket for specified robot.""" |
| 195 | + parser = argparse.ArgumentParser(description="Pulls run logs from ABR robots.") |
| 196 | + parser.add_argument( |
| 197 | + "storage_directory", |
| 198 | + metavar="STORAGE_DIRECTORY", |
| 199 | + type=str, |
| 200 | + nargs=1, |
| 201 | + help="Path to long term storage directory for run logs.", |
| 202 | + ) |
| 203 | + parser.add_argument( |
| 204 | + "robot_ip", |
| 205 | + metavar="ROBOT_IP", |
| 206 | + type=str, |
| 207 | + nargs=1, |
| 208 | + help="IP address of robot as string.", |
| 209 | + ) |
| 210 | + parser.add_argument( |
| 211 | + "jira_api_token", |
| 212 | + metavar="JIRA_API_TOKEN", |
| 213 | + type=str, |
| 214 | + nargs=1, |
| 215 | + help="JIRA API Token. Get from https://id.atlassian.com/manage-profile/security.", |
| 216 | + ) |
| 217 | + parser.add_argument( |
| 218 | + "email", |
| 219 | + metavar="EMAIL", |
| 220 | + type=str, |
| 221 | + nargs=1, |
| 222 | + help="Email connected to JIRA account.", |
| 223 | + ) |
| 224 | + # TODO: write function to get reporter_id from email. |
| 225 | + parser.add_argument( |
| 226 | + "reporter_id", |
| 227 | + metavar="REPORTER_ID", |
| 228 | + type=str, |
| 229 | + nargs=1, |
| 230 | + help="JIRA Reporter ID.", |
| 231 | + ) |
| 232 | + # TODO: improve help comment on jira board id. |
| 233 | + parser.add_argument( |
| 234 | + "board_id", |
| 235 | + metavar="BOARD_ID", |
| 236 | + type=str, |
| 237 | + nargs=1, |
| 238 | + help="JIRA Board ID. RABR is 217", |
| 239 | + ) |
| 240 | + args = parser.parse_args() |
| 241 | + storage_directory = args.storage_directory[0] |
| 242 | + ip = args.robot_ip[0] |
| 243 | + url = "https://opentrons.atlassian.net" |
| 244 | + api_token = args.jira_api_token[0] |
| 245 | + email = args.email[0] |
| 246 | + board_id = args.board_id[0] |
| 247 | + reporter_id = args.reporter_id[0] |
| 248 | + ticket = JiraTicket(url, api_token, email) |
| 249 | + error_runs = get_error_runs_from_robot(ip) |
| 250 | + one_run = error_runs[-1] # Most recent run with error. |
| 251 | + ( |
| 252 | + summary, |
| 253 | + robot, |
| 254 | + affects_version, |
| 255 | + components, |
| 256 | + whole_description_str, |
| 257 | + saved_file_path, |
| 258 | + ) = get_error_info_from_robot(ip, one_run, storage_directory) |
| 259 | + print(f"Making ticket for run: {one_run} on robot {robot}.") |
| 260 | + # TODO: make argument or see if I can get rid of with using board_id. |
| 261 | + project_key = "RABR" |
| 262 | + parent_key = project_key + "-" + robot[-1] |
| 263 | + issue_url, issue_key = ticket.create_ticket( |
| 264 | + summary, |
| 265 | + whole_description_str, |
| 266 | + project_key, |
| 267 | + reporter_id, |
| 268 | + "Bug", |
| 269 | + "Medium", |
| 270 | + components, |
| 271 | + affects_version, |
| 272 | + parent_key, |
| 273 | + ) |
| 274 | + ticket.open_issue(issue_key) |
| 275 | + ticket.post_attachment_to_ticket(issue_key, saved_file_path) |
0 commit comments