Skip to content

Commit ad5650d

Browse files
authored
ABR JIRA TICKET CREATION. (#14767)
<!-- Thanks for taking the time to open a pull request! Please make sure you've read the "Opening Pull Requests" section of our Contributing Guide: https://github.com/Opentrons/opentrons/blob/edge/CONTRIBUTING.md#opening-pull-requests To ensure your code is reviewed quickly and thoroughly, please fill out the sections below to the best of your ability! --> # Overview Automate JIRA Ticket Creation Process for robots with errors. # Test Plan - used with ABR robots when errors have occurred. Tickets have been recorded accurately. # Changelog - Removed uncertain error levels in error_levels to allow for "Level # Failure" component to be filled in on JIRA - Created jira_tools function to create tickets, open tickets, collect error information from robot, read issues on board - jira_tools function add_attachments_to_ticket currently results in an error. The run log is saved as a file on your computer but cannot be added to the ticket . - added arguments for JIRA api key, storage directory, robot ip, email, board id # Review requests # Risk assessment - JIRA api token was acciedntly uploaded in previous merges but the token has been retired. - add_attachments_to_ticket does not currently work. - This script will only work if you run it before the errored out robot starts another run. - RABR is currently hard coded as the board to post to.
1 parent bb33f7c commit ad5650d

10 files changed

+298
-13
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,275 @@
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)

abr-testing/abr_testing/data_collection/abr_google_drive.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
from datetime import datetime, timedelta
88
from abr_testing.data_collection import read_robot_logs
99
from typing import Set, Dict, Any
10-
from abr_testing.google_automation import google_drive_tool, google_sheets_tool
10+
from abr_testing.automation import google_drive_tool, google_sheets_tool
1111

1212

1313
def get_modules(file_results: Dict[str, str]) -> Dict[str, Any]:

abr-testing/abr_testing/data_collection/error_levels.csv

+6-6
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ Prefix,Error Code,Description,Categories,Level of Failure,
1111
2,2000,Robotics Control Error,A Robot Action Failed,3,
1212
2,2001,Motion Failed,A Robot Action Failed,4,
1313
2,2002,Homing Failed,A Robot Action Failed,4,
14-
2,2003,Stall or Collision Detected,A Robot Action Failed,3-4,
14+
2,2003,Stall or Collision Detected,A Robot Action Failed,3,
1515
2,2004,Motion Planning Failed,A Robot Action Failed,3,
1616
2,2005,Position Estimation Invalid,A Robot Action Failed,3,
1717
2,2006,Move Condition Not Met,A Robot Action Failed,3,
@@ -22,15 +22,15 @@ Prefix,Error Code,Description,Categories,Level of Failure,
2222
2,2011,Misaligned Gantry,A Robot Action Failed,3,
2323
2,2012,Unmatched Tip Presence States,A Robot Action Failed,3-4,
2424
2,2013,Position Unknown,A Robot Action Failed,4,
25-
2,2014,Execution Cancelled,A Robot Action Failed,3-4,
26-
2,2015,Failed Gripper Pickup Error,A Robot Action Failed,3-4,
25+
2,2014,Execution Cancelled,A Robot Action Failed, 4,
26+
2,2015,Failed Gripper Pickup Error,A Robot Action Failed,3,
2727
3,3000,Robotics Interaction Error,A Robot Interaction Failed,3,
28-
3,3001,Labware Dropped,A Robot Interaction Failed,3-4,
29-
3,3002,Labware Not Picked Up,A Robot Interaction Failed,3-4,
28+
3,3001,Labware Dropped,A Robot Interaction Failed, 4,
29+
3,3002,Labware Not Picked Up,A Robot Interaction Failed,4,
3030
3,3003,Tip Pickup Failed,A Robot Interaction Failed,4,
3131
3,3004,Tip Drop Failed,A Robot Interaction Failed,4,
3232
3,3005,Unexpeted Tip Removal,A Robot Interaction Failed,4,
33-
3,3006,Pipette Overpressure,A Robot Interaction Failed,3-4,
33+
3,3006,Pipette Overpressure,A Robot Interaction Failed,3,
3434
3,3008,E-Stop Activated,A Robot Interaction Failed,Not an error,
3535
3,3009,E-Stop Not Present,A Robot Interaction Failed,5,
3636
3,3010,Pipette Not Present,A Robot Interaction Failed,5,

abr-testing/abr_testing/data_collection/get_run_logs.py

+4-4
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
import requests
77
import sys
88
from abr_testing.data_collection import read_robot_logs
9-
from abr_testing.google_automation import google_drive_tool
9+
from abr_testing.automation import google_drive_tool
1010

1111

1212
def get_run_ids_from_robot(ip: str) -> Set[str]:
@@ -80,9 +80,9 @@ def save_runs(runs_to_save: Set[str], ip: str, storage_directory: str) -> Set[st
8080
saved_file_paths = set()
8181
for a_run in runs_to_save:
8282
data = get_run_data(a_run, ip)
83-
data_file_name = ip + "_" + data["run_id"] + ".json"
84-
saved_file_path = os.path.join(storage_directory, data_file_name)
85-
json.dump(data, open(saved_file_path, mode="w"))
83+
saved_file_path = read_robot_logs.save_run_log_to_json(
84+
ip, data, storage_directory
85+
)
8686
saved_file_paths.add(saved_file_path)
8787
print(f"Saved {len(runs_to_save)} run(s) from robot with IP address {ip}.")
8888
return saved_file_paths

abr-testing/abr_testing/data_collection/read_robot_logs.py

+10
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,16 @@ def get_unseen_run_ids(runs: Set[str], runs_from_storage: Set[str]) -> Set[str]:
138138
return runs_to_save
139139

140140

141+
def save_run_log_to_json(
142+
ip: str, results: Dict[str, Any], storage_directory: str
143+
) -> str:
144+
"""Save run log to local json file."""
145+
data_file_name = ip + "_" + results["run_id"] + ".json"
146+
saved_file_path = os.path.join(storage_directory, data_file_name)
147+
json.dump(results, open(saved_file_path, mode="w"))
148+
return saved_file_path
149+
150+
141151
def get_run_ids_from_google_drive(google_drive: Any) -> Set[str]:
142152
"""Get run ids in google drive folder."""
143153
# Run ids in google_drive_folder

abr-testing/abr_testing/tools/abr_asair_sensor.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
import time as t
77
from typing import List
88
import argparse
9-
from abr_testing.google_automation import google_sheets_tool
9+
from abr_testing.automation import google_sheets_tool
1010

1111

1212
class _ABRAsairSensor:

abr-testing/abr_testing/tools/abr_scale.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
import argparse
88
import csv
99
from abr_testing.data_collection import read_robot_logs
10-
from abr_testing.google_automation import google_sheets_tool
10+
from abr_testing.automation import google_sheets_tool
1111

1212

1313
def write_to_sheets(file_name_csv: str, google_sheet: Any, row_list: List) -> None:

0 commit comments

Comments
 (0)