Merge ~weichenwu/bugit:file-issue-to-JIRA into bugit:master

Proposed by Weichen Wu
Status: Merged
Approved by: StanleyHuang
Approved revision: eec56427f0b85971ada3d999ed5bdf8b9aea12fb
Merged at revision: c796f1d2041fac6aadf4e1ce578e73b012fd1d8e
Proposed branch: ~weichenwu/bugit:file-issue-to-JIRA
Merge into: bugit:master
Diff against target: 1219 lines (+618/-265)
6 files modified
README.md (+26/-4)
bugit/__init__.py (+55/-45)
bugit/bug_assistant.py (+143/-211)
bugit/helper/jira_helper.py (+151/-0)
bugit/helper/launchpad_helper.py (+234/-0)
bugit/ui.py (+9/-5)
Reviewer Review Type Date Requested Status
StanleyHuang Approve
HanhsuanLee Approve
PeiYao Chang Pending
OEM Services QA Pending
Review via email: mp+461788@code.launchpad.net

Description of the change

Provide an option to file an issue on JIRA

Note:
By default, the issue will go to Launchpad as we still mainly use Launchpad at the moment.

To post a comment you must log in.
Revision history for this message
StanleyHuang (stanley31) wrote :

@Weichen,

I would suggest we modify the way to initial Jira/LaunchpadAssistant, please see my inline comment.

Revision history for this message
StanleyHuang (stanley31) :
review: Needs Information
Revision history for this message
HanhsuanLee (hanhsuan) wrote :

Please see my inline comment

Revision history for this message
HanhsuanLee (hanhsuan) wrote :

LGTM + 1

review: Approve
Revision history for this message
StanleyHuang (stanley31) wrote :

LGTM

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/README.md b/README.md
2index d6332a2..9ceaf40 100644
3--- a/README.md
4+++ b/README.md
5@@ -18,7 +18,7 @@ $ snap install bugit --devmode
6
7
8 Usage
9------
10+------------
11
12 $ sudo bugit
13
14@@ -26,9 +26,24 @@ You will be presented with a text user interface (TUI) to fill the important
15 information (bug title, description, status, etc.).
16
17 You can navigate between the different fields using the `up` and `down` keys
18-and press `Alt+Enter` to submit the issue. It will be created on Launchpad, and
19-logs will be gathered (using reporting tool `sosreport`) and attached to the
20-report.
21+and press `Alt+Enter` to submit the issue. All logs will be gathered
22+(using reporting tool `sosreport`) and attached to the report.
23+
24+By default, the issue will be created on Launchpad.
25+If you wish to create issue on JIRA, use:
26+
27+$ sudo bugit --jira
28+
29+Note: for issus to jira, the assignee could be entered partial unitl it has only one match
30+For example, user email is weichen.wu@canonical.com
31+You can enter "wei", it will output
32+"""
33+weichen.wu@canonical.com
34+weii.wang@canonical.com
35+yao.wei@canonical.com
36+"""
37+
38+By entering "weichen", it will only match the first one hence successfully assigned
39
40 If you want to submit your issue to the Staging instance of Launchpad, use:
41
42@@ -48,3 +63,10 @@ $ sudo bugit -p checkbox-ng -t "ce-qa-concern audio hdmi"
43 If you want to reopen an issue, use:
44
45 $ sudo bugit -p checkbox-ng -r 2000334
46+
47+
48+Reference
49+------------
50+1. How to access a file in /var/lib
51+Refer to this topic for details
52+https://forum.snapcraft.io/t/read-access-to-a-file-in-var-lib-on-the-host/11766
53\ No newline at end of file
54diff --git a/bugit/__init__.py b/bugit/__init__.py
55index cd4d8eb..40d77fc 100644
56--- a/bugit/__init__.py
57+++ b/bugit/__init__.py
58@@ -3,10 +3,11 @@ import logging
59 import os
60 import sys
61
62-from bugit.bug_assistant import BugAssistant, BugReport
63+from bugit.bug_assistant import BugReport
64 from bugit.ui import (
65 JobsScreen, SessionsScreen, ReportScreen, ReopenReportScreen
66 )
67+
68 import bugit.utils
69 import bugit.checkbox_utils
70
71@@ -16,7 +17,8 @@ logger = logging.getLogger(__name__)
72
73 def main():
74 if os.geteuid() != 0:
75- sys.exit("This tool MUST be run as super user. Please run `sudo bugit`.")
76+ sys.exit("This tool MUST be run as super user. \
77+ Please run `sudo bugit`.")
78 snap_name = os.getenv("SNAP_NAME")
79
80 if not snap_name:
81@@ -42,7 +44,9 @@ def main():
82 numeric_level = getattr(logging, loglevel.upper(), None)
83 if not isinstance(numeric_level, int):
84 raise ValueError("Invalid log level: {}".format(loglevel))
85- logging.basicConfig(filename=logfile, format=logformat, level=numeric_level)
86+ logging.basicConfig(filename=logfile,
87+ format=logformat,
88+ level=numeric_level)
89
90 epilog = (
91 "Examples: `bugit --fwts -p somerville -t platform-tag-3` will "
92@@ -55,10 +59,12 @@ def main():
93 parser = argparse.ArgumentParser(epilog=epilog, argument_default="")
94 parser.add_argument(
95 "--fwts",
96- help=("Automatically files an issue in " "Launchpad for the firmware team"),
97+ help=("Automatically files an issue in "
98+ "Launchpad for the firmware team"),
99 action="store_true",
100 )
101- parser.add_argument("-a", "--assignee", help="Assignee e.g. your Launchpad ID")
102+ parser.add_argument("-a", "--assignee",
103+ help="Assignee e.g. Launchpad ID or Jira account name")
104 parser.add_argument("-p", "--project", help="Project name")
105 parser.add_argument("-s", "--series", help="Series e.g. focal")
106 parser.add_argument(
107@@ -72,6 +78,11 @@ def main():
108 "-r", "--reopen", help="Bug number you want to reopen",
109 type=str, default=""
110 )
111+ parser.add_argument(
112+ "--jira",
113+ help=("File bug to JIRA"),
114+ action="store_true",
115+ )
116 args = parser.parse_args()
117 if args.fwts:
118 if not args.project:
119@@ -84,15 +95,16 @@ def main():
120 if not args.project:
121 sys.exit("You must specify a project name, e.g -p somerville")
122
123- if not args.reopen.isnumeric():
124- sys.exit("Invalid bug number format: {}".format(args.reopen))
125+ # if not args.reopen.isnumeric():
126+ # sys.exit("Invalid bug number format: {}".format(args.reopen))
127
128 start_reopen_ui(
129- verify_bug_information(args.reopen, args.project),
130+ args.reopen,
131 args.project,
132 assignee=args.assignee,
133 sku=args.sku,
134- cid=args.cid
135+ cid=args.cid,
136+ template="Jira" if args.jira else "Launchpad"
137 )
138 else:
139 start_ui(
140@@ -102,10 +114,23 @@ def main():
141 tags=args.tags,
142 sku=args.sku,
143 cid=args.cid,
144+ template="Jira" if args.jira else "Launchpad"
145 )
146
147
148-def start_ui(assignee="", project="", series="", tags="", sku="", cid=""):
149+def get_target_ba(template):
150+ """ Get target ba, launchpad or jira
151+ """
152+ target_cls_name = f"{template}BugAssistant"
153+ mod = __import__('bugit.bug_assistant', fromlist=[target_cls_name])
154+ target_cls = getattr(mod, target_cls_name)
155+ ba = target_cls()
156+
157+ return ba
158+
159+
160+def start_ui(assignee="", project="", series="",
161+ tags="", sku="", cid="", template=""):
162 tags += " {}".format(cid)
163 tags = " ".join(tags.split()) # Remove unwanted spaces
164 job_id = ""
165@@ -115,7 +140,8 @@ def start_ui(assignee="", project="", series="", tags="", sku="", cid=""):
166 session_screen = SessionsScreen(items=checkbox_sessions_dir)
167 session_screen.run()
168 if session_screen.selected_item != "None":
169- jobs = bugit.checkbox_utils.Session(path=session_screen.selected_item)
170+ jobs = bugit.checkbox_utils.Session(
171+ path=session_screen.selected_item)
172 jobs_screen = JobsScreen(items=jobs.get_run_jobs())
173 jobs_screen.run()
174 if jobs_screen.selected_item != "None":
175@@ -124,16 +150,18 @@ def start_ui(assignee="", project="", series="", tags="", sku="", cid=""):
176
177 report_screen = ReportScreen(
178 assignee=assignee.lower(),
179- project=project.lower(),
180+ project=project,
181 series=series.lower(),
182 tags=tags.lower(),
183 sku=sku.strip().upper(),
184 cid=cid,
185 job_id=job_id,
186 job_output=job_output,
187+ template=template.lower()
188 )
189 report_screen.run()
190- ba = BugAssistant()
191+
192+ ba = get_target_ba(template)
193 ba.update(report_screen.report())
194 while True:
195 try:
196@@ -147,34 +175,12 @@ def start_ui(assignee="", project="", series="", tags="", sku="", cid=""):
197 break
198
199
200-def verify_bug_information(bug_no, project):
201- ba = BugAssistant()
202- ba.launchpad_login()
203- try:
204- bug = ba.get_bug(bug_no)
205- bug_link = ba.get_bug_attribute(bug, "self_link")
206- except KeyError:
207- raise ValueError("Invalid bug number: {}".format(bug_no))
208+def start_reopen_ui(bug_id, project, assignee="", sku="", cid="", template=""):
209
210- project_bugs = ba.get_project_bugs_by_title(
211- project, ba.get_bug_attribute(bug, "title")
212- )
213-
214- for p_bug in project_bugs:
215- if ba.get_bug_attribute(p_bug, "bug_link") == bug_link:
216- ba.bug = bug
217- break
218-
219- if ba.bug is None:
220- sys.exit("Bug not exists in {} project: {}".format(project, bug_no))
221-
222- return ba
223-
224-
225-def start_reopen_ui(ba, project, assignee="", sku="", cid=""):
226- # collect tags from original bug
227- bug_info = ba.get_bug_infomation()
228- tags = "{} {}".format(bug_info["tags"], cid).strip()
229+ ba = get_target_ba(template)
230+ bug, bug_info = ba.bug_helper.get_bug_information(bug_id)
231+ tags = "{} {}".format(bug_info["tags"], cid).strip() \
232+ if template == "launchpad" else ""
233
234 job_id = ""
235 job_output = None
236@@ -183,7 +189,8 @@ def start_reopen_ui(ba, project, assignee="", sku="", cid=""):
237 session_screen = SessionsScreen(items=checkbox_sessions_dir)
238 session_screen.run()
239 if session_screen.selected_item != "None":
240- jobs = bugit.checkbox_utils.Session(path=session_screen.selected_item)
241+ jobs = bugit.checkbox_utils.Session(
242+ path=session_screen.selected_item)
243 jobs_screen = JobsScreen(items=jobs.get_run_jobs())
244 jobs_screen.run()
245 if jobs_screen.selected_item != "None":
246@@ -192,16 +199,18 @@ def start_reopen_ui(ba, project, assignee="", sku="", cid=""):
247
248 report_screen = ReopenReportScreen(
249 assignee=assignee.lower() or bug_info["assignee"],
250- project=project.lower(),
251+ project=project,
252 sku=sku.strip().upper(),
253 cid=cid,
254 title=bug_info["title"],
255 tags=tags,
256 importance=bug_info["importance"],
257 job_id=job_id,
258- job_output=job_output
259+ job_output=job_output,
260+ template=template.lower()
261 )
262 report_screen.run()
263+
264 ba.update(report_screen.report())
265 while True:
266 try:
267@@ -215,7 +224,7 @@ def start_reopen_ui(ba, project, assignee="", sku="", cid=""):
268 break
269
270
271-def auto_submit_fwts_report(project, tags):
272+def auto_submit_fwts_report(project, tags, template=""):
273 """
274 Automatically file a Launchpad issue with log files needed by FWTS team
275 without starting the UI.
276@@ -233,7 +242,8 @@ def auto_submit_fwts_report(project, tags):
277 importance="Critical",
278 options=options,
279 )
280- ba = BugAssistant(report)
281+
282+ ba = get_target_ba(template)
283 try:
284 result = ba.file_bug()
285 except Exception as exc:
286diff --git a/bugit/bug_assistant.py b/bugit/bug_assistant.py
287index da778b1..4592fb0 100644
288--- a/bugit/bug_assistant.py
289+++ b/bugit/bug_assistant.py
290@@ -1,15 +1,16 @@
291-import getpass
292 import os
293 import re
294 import subprocess
295 import sys
296 import tarfile
297+import io
298 from collections import Counter
299 from glob import glob
300-from launchpadlib.launchpad import Launchpad
301-from launchpadlib.uris import LPNET_WEB_ROOT, STAGING_WEB_ROOT, QASTAGING_WEB_ROOT
302
303-import bugit.checkbox_utils
304+from bugit.helper.launchpad_helper import LaunchpadAssistant
305+from bugit.helper.jira_helper import JiraAssistant
306+
307+from bugit.checkbox_utils import get_checkbox_last_session_dir
308 import bugit.utils
309
310
311@@ -68,220 +69,157 @@ class BugReport:
312 self.options = options
313
314
315-class BugAssistant:
316- """Handle a bug report, its attachments and their transmission to Launchpad."""
317+class LaunchpadBugAssistant:
318+ """Handle a bug report, its attachments and their transmission
319+ to Launchpad.
320+ """
321
322 def __init__(self):
323- #self.update(bug_report)
324- # Launchpad bug, in case something is wrong after creating the bug
325 self.bug = None
326- self.launchpad = None
327+ self.bug_helper = LaunchpadAssistant()
328
329 def update(self, bug_report):
330- self.lp_title = bug_report.title
331- self.lp_description = bug_report.description
332- self.lp_project = bug_report.project
333- self.lp_series = bug_report.series
334- self.lp_assignee = bug_report.assignee
335- self.lp_tags = bug_report.tags
336- self.lp_status = bug_report.status
337- self.lp_importance = bug_report.importance
338+ self.title = bug_report.title
339+ self.description = bug_report.description
340+ self.project = bug_report.project
341+ self.series = bug_report.series
342+ self.assignee = bug_report.assignee
343+ self.tags = bug_report.tags
344+ self.status = bug_report.status
345+ self.importance = bug_report.importance
346 self.options = bug_report.options
347
348- def launchpad_login(self):
349- """Log in Launchpad and create credentials file if needed."""
350- # can be 'production', 'staging' or 'qastaging'
351- service_root = os.environ.get("APPORT_LAUNCHPAD_INSTANCE", "production")
352- print("Using {} service root".format(service_root))
353-
354- directory = os.path.expanduser("~/.launchpadlib")
355- if not os.path.isdir(directory):
356- os.mkdir(directory)
357-
358- launchpad = Launchpad.login_with(
359- "bugit",
360- service_root,
361- credentials_file=os.path.join(directory, "bugit"),
362- allow_access_levels=["WRITE_PRIVATE"],
363- )
364-
365- # Small trick to force access to launchpad and verify authentication
366- launchpad.me
367- self.launchpad = launchpad
368-
369- def get_bug_infomation(self):
370- if self.bug is None:
371- raise BugAssistantError("Bug is not defined")
372-
373- bug_task = self.bug.bug_tasks[0]
374- bug_info = {
375- "status": bug_task.status,
376- "title": bug_task.title,
377- "tags": " ".join(self.bug.lp_get_parameter("tags")),
378- "assignee": bug_task.assignee.name if bug_task.assignee else "",
379- "importance": bug_task.importance
380- }
381-
382- return bug_info
383-
384 def file_bug(self):
385 """Upload bug report and its attachments to Launchpad."""
386
387- if not self.lp_title:
388+ if not self.title:
389 raise BugAssistantError("Bug title is mandatory")
390- if not self.lp_description:
391+ if not self.description:
392 raise BugAssistantError("Description is mandatory")
393- if not self.lp_project:
394+ if not self.project:
395 raise BugAssistantError("Project name is mandatory")
396
397 print("Logging into Launchpad...")
398- self.launchpad_login()
399- launchpad = self.launchpad
400+ launchpad = LaunchpadAssistant()
401+ bug_dict = {
402+ 'assignee': self.assignee,
403+ 'project': self.project,
404+ 'title': self.title,
405+ 'description': self.description,
406+ 'priority': self.importance,
407+ 'status': self.status,
408+ 'tags': self.tags,
409+ 'series': self.series
410+ }
411+ self.bug, bug_url = launchpad.create_bug(bug_dict)
412+ print("Uploading bug data...")
413+ aa = AttachmentAssistant(self.options)
414+ attachments = aa.run_attachment_methods()
415+ launchpad.upload_attachments(attachments)
416
417- try:
418- print("Checking project name...")
419- project = launchpad.projects[self.lp_project]
420- except Exception:
421- error_message = "{} launchpad project not found".format(self.lp_project)
422- raise BugAssistantError(error_message)
423-
424- series_name = self.lp_series
425- series = None
426- if series_name:
427- print("Checking series...")
428- series = project.getSeries(name=series_name)
429- if series is None:
430- error_message = "{} series not found".format(series_name)
431- raise BugAssistantError(error_message)
432-
433- assignee_name = self.lp_assignee
434- assignee = None
435- if assignee_name:
436- try:
437- print("Checking assignee...")
438- assignee = launchpad.people[assignee_name]
439- except Exception:
440- error_message = "{} launchpad user not found".format(assignee_name)
441- raise BugAssistantError(error_message)
442-
443- if not self.bug:
444- print("Creating Launchpad bug report...")
445- self.bug = launchpad.bugs.createBug(
446- title=self.lp_title,
447- description=self.lp_description,
448- tags=self.lp_tags.split(),
449- target=project,
450- )
451- print("Bug report #{} created.".format(self.bug.id))
452- else:
453- print("Updating Launchpad bug report...")
454- self.bug.title = self.lp_title
455- self.bug.description = self.lp_description
456- self.bug.tags = self.lp_tags.split()
457- self.bug.lp_save()
458-
459- # Task configuration
460- task = self.bug.bug_tasks[0]
461- if self.bug and task.target != project:
462- print("Updating project...")
463- task.target = project
464- task.lp_save()
465-
466- if series:
467- print("Setting series...")
468- nomination = self.bug.addNomination(target=series)
469- nomination.approve()
470-
471- # We update bug info only for the latest created series
472- task = self.bug.bug_tasks[len(self.bug.bug_tasks) - 1]
473- if assignee:
474- print("Setting assignee for series {}...".format(task.bug_target_name))
475- task.assignee = assignee
476- print("Setting status...")
477- task.status = self.lp_status
478- print("Setting importance...")
479- task.importance = self.lp_importance
480-
481- task.lp_save()
482-
483- service_root = os.environ.get("APPORT_LAUNCHPAD_INSTANCE", "production")
484- if service_root == "qastaging":
485- bug_url = QASTAGING_WEB_ROOT + "bugs/{}".format(self.bug.id)
486- elif service_root == "staging":
487- bug_url = STAGING_WEB_ROOT + "bugs/{}".format(self.bug.id)
488- else:
489- bug_url = LPNET_WEB_ROOT + "bugs/{}".format(self.bug.id)
490+ result = "Bug report #{} and its attachments are available at <{}>"
491+ return result.format(self.bug.id, bug_url)
492
493- print("Bug report #{} updated.".format(self.bug.id))
494+ def reopen(self):
495+ """ Reopen a bug on launchpad
496
497- print("Uploading bug data...")
498+ Args:
499+ launchpad: launchpadAssistant instance
500+ """
501+ bug_dict = {
502+ 'assignee': self.assignee,
503+ 'project': self.project,
504+ 'title': self.title,
505+ 'description': self.description,
506+ 'priority': self.importance,
507+ 'status': self.status,
508+ 'tags': self.tags,
509+ 'series': self.series
510+ }
511+ launchpad = self.bug_helper
512+ # add comment and update assignee/status/importance/tags
513+ launchpad.add_comment(bug_dict['description'])
514+ self.bug, bug_url = launchpad.update_bug(bug_dict)
515+
516+ # upload attachment
517 aa = AttachmentAssistant(self.options)
518- aa.run_attachment_methods()
519- aa.upload_attachments(launchpad, self.bug.id)
520+ attachments = aa.run_attachment_methods()
521+ launchpad.upload_attachments(attachments)
522
523 result = "Bug report #{} and its attachments are available at <{}>"
524 return result.format(self.bug.id, bug_url)
525
526- def reopen(self):
527- assignee_name = self.lp_assignee
528- assignee = None
529- if assignee_name:
530- try:
531- print("Checking assignee...")
532- assignee = self.launchpad.people[assignee_name]
533- except Exception:
534- error_message = "{} launchpad user not found".format(
535- assignee_name)
536- raise BugAssistantError(error_message)
537-
538- bug_task = self.bug.bug_tasks[0]
539- self.add_comment(self.lp_description)
540- if assignee:
541- print("Setting assignee to {}...".format(bug_task.assignee))
542- bug_task.assignee = assignee
543- print("Setting status...")
544- bug_task.status = self.lp_status
545- print("Setting importance...")
546- bug_task.importance = self.lp_importance
547-
548- bug_task.lp_save()
549-
550- self.bug.tags = self.lp_tags.split()
551- self.bug.lp_save()
552-
553- service_root = os.environ.get("APPORT_LAUNCHPAD_INSTANCE", "production")
554- if service_root == "qastaging":
555- bug_url = QASTAGING_WEB_ROOT + "bugs/{}".format(self.bug.id)
556- elif service_root == "staging":
557- bug_url = STAGING_WEB_ROOT + "bugs/{}".format(self.bug.id)
558- else:
559- bug_url = LPNET_WEB_ROOT + "bugs/{}".format(self.bug.id)
560
561- print("Bug report #{} updated.".format(self.bug.id))
562+class JiraBugAssistant:
563+ def __init__(self):
564+ self.bug = None
565+ self.bug_helper = JiraAssistant()
566+
567+ def update(self, bug_report):
568+ self.title = bug_report.title
569+ self.description = bug_report.description
570+ self.project = bug_report.project
571+ self.assignee = bug_report.assignee
572+ self.tags = bug_report.tags
573+ self.status = bug_report.status
574+ self.importance = bug_report.importance
575+ self.options = bug_report.options
576+
577+ def file_bug(self):
578+ """Upload bug report and its attachments to JIRA."""
579+
580+ if not self.title:
581+ raise BugAssistantError("Bug title is mandatory")
582+ if not self.description:
583+ raise BugAssistantError("Description is mandatory")
584+ if not self.project:
585+ raise BugAssistantError("Project name is mandatory")
586
587+ print("Logging into Jira...")
588+ jira = JiraAssistant()
589+ bug_dict = {
590+ 'assignee': self.assignee,
591+ 'project': self.project,
592+ 'summary': self.title,
593+ 'description': self.description,
594+ 'priority': {'name': self.importance},
595+ 'issuetype': {'name': 'Bug'}
596+ }
597+ self.bug, bug_url = jira.create_bug(bug_dict)
598 print("Uploading bug data...")
599 aa = AttachmentAssistant(self.options)
600- aa.run_attachment_methods()
601- aa.upload_attachments(self.launchpad, self.bug.id)
602+ attachments = aa.run_attachment_methods()
603+ jira.upload_attachments(attachments)
604
605 result = "Bug report #{} and its attachments are available at <{}>"
606- return result.format(self.bug.id, bug_url)
607+ return result.format(self.bug.key, bug_url)
608
609- def add_comment(self, data):
610- self.bug.newMessage(content=data)
611+ def reopen(self):
612+ """ Reopen a bug on Jira
613
614- def get_bug(self, bug_id):
615- return self.launchpad.bugs[bug_id]
616+ Args:
617+ jira: JiraAssistant instance
618+ """
619
620- def get_project_bugs_by_title(self, project, title):
621- lp_project = self.launchpad.projects[project]
622- project_bugs = lp_project.searchTasks(
623- search_text=title
624- )
625- return project_bugs
626+ bug_dict = {
627+ 'assignee': self.assignee,
628+ 'project': self.project,
629+ 'summary': self.title,
630+ 'description': self.description,
631+ 'priority': {'name': self.importance}
632+ }
633+ jira = self.bug_helper
634+ # add comment and update assignee/importance
635+ jira.add_comment(bug_dict['description'])
636+ self.bug, bug_url = jira.update_bug(bug_dict)
637+
638+ # upload attachment
639+ aa = AttachmentAssistant(self.options)
640+ attachments = aa.run_attachment_methods()
641+ jira.upload_attachments(attachments)
642
643- def get_bug_attribute(self, bug, attr):
644- return bug.lp_get_parameter(attr)
645+ result = "Bug report #{} and its attachments are available at <{}>"
646+ return result.format(self.bug.key, bug_url)
647
648
649 class BugAssistantError(Exception):
650@@ -302,15 +240,6 @@ class AttachmentAssistant:
651 self.attachments = {}
652 self.options = options
653
654- def upload_attachments(self, lp_instance, bug_number):
655- attachments = self.attachments
656- for a in attachments:
657- print("Uploading attachment {}...".format(a))
658- bug = lp_instance.bugs[bug_number]
659- bug.addAttachment(
660- comment="Automatically attached", filename=a, data=attachments[a]
661- )
662-
663 def run_attachment_methods(self):
664 """Runs the different attachment methods to add logs to the
665 attachments dictionary."""
666@@ -326,6 +255,8 @@ class AttachmentAssistant:
667 if self.options.get("collect_log"):
668 self.attach_log_report()
669
670+ return self.attachments
671+
672 def attach_log_report(self):
673 home_dir = os.getenv("HOME")
674 print("Running log report...")
675@@ -337,7 +268,7 @@ class AttachmentAssistant:
676 with open(report_name, mode="rb") as f:
677 data = f.read()
678 self.attachments[report_name] = data
679- except subprocess.CalledProcessError as e:
680+ except subprocess.CalledProcessError:
681 print("Failed to collect log by oem-getlogs, try sosreport...")
682 try:
683 command = [
684@@ -385,11 +316,13 @@ class AttachmentAssistant:
685 devmode,
686 snap.props.confinement.value_nick,
687 )
688- self.attachments["snap_list.log"] = out.encode()
689+ # Note: jiralib's add_attachment can only handle file-like object
690+ data = io.BytesIO(out.encode())
691+ self.attachments["snap_list.log"] = data.read()
692
693 def attach_last_checkbox_session(self):
694 """Archive and attach the most recent Checkbox session available."""
695- latest_session_dir = bugit.checkbox_utils.get_checkbox_last_session_dir()
696+ latest_session_dir = get_checkbox_last_session_dir()
697 if latest_session_dir:
698 session_tgz = os.path.expandvars("$HOME/checkbox-session.tgz")
699 if os.path.exists(session_tgz):
700@@ -405,9 +338,10 @@ class AttachmentAssistant:
701 and attach them.
702 """
703
704- latest_session_dir = bugit.checkbox_utils.get_checkbox_last_session_dir()
705+ latest_session_dir = get_checkbox_last_session_dir()
706 if latest_session_dir:
707- logpath = os.path.join(latest_session_dir, "CHECKBOX_DATA", "fwts*.log")
708+ logpath = os.path.join(
709+ latest_session_dir, "CHECKBOX_DATA", "fwts*.log")
710 logfiles = glob(logpath)
711 if logfiles:
712 for log in logfiles:
713@@ -417,7 +351,8 @@ class AttachmentAssistant:
714 else:
715 print("No FWTS logs found in Checkbox session!")
716 else:
717- print(("Cannot find Checkbox session where FWTS logs could be " "located!"))
718+ print("Cannot find Checkbox session where"
719+ "FWTS logs could be located!")
720
721 def attach_nvidia_bug_report(self):
722
723@@ -445,14 +380,10 @@ class AttachmentAssistant:
724 def attach_acpidump(self):
725 print("Running acpidump...")
726 command = ["acpidump"]
727- process = subprocess.run(
728- command, stdout=subprocess.PIPE, stderr=subprocess.PIPE
729- )
730- if process.returncode == 0:
731- self.attachments["acpidump.log"] = process.stdout
732- else:
733- print("Error while running acpidump:")
734- print(process.stderr)
735+ acpi_output = issue_commands(command)
736+ if acpi_output is not None:
737+ data = io.BytesIO(acpi_output.encode())
738+ self.attachments["acpidump.log"] = data.read()
739
740 def attach_previous_kernel_log(self):
741 print("Dumping previous kernel log...")
742@@ -486,8 +417,8 @@ class AttachmentAssistant:
743 standard_info = {}
744
745 buildstamp_paths = [
746- # For how to access a file in /var/lib
747- # See: https://forum.snapcraft.io/t/read-access-to-a-file-in-var-lib-on-the-host/11766
748+ # For how to access a file in /var/lib,
749+ # please find reference in README
750 "/var/lib/snapd/hostfs/var/lib/ubuntu_dist_channel", # PC project
751 "/var/lib/snapd/hostfs/.disk/info", # ubuntu classic
752 "/run/mnt/ubuntu-seed/.disk/info", # ubuntu core
753@@ -578,7 +509,8 @@ class AttachmentAssistant:
754 cpu_names[cpu_name] += 1
755
756 cpu_names_str = [
757- "{} ({}x)".format(cpu_name, count) for cpu_name, count in cpu_names.items()
758+ "{} ({}x)".format(cpu_name, count)
759+ for cpu_name, count in cpu_names.items()
760 ]
761
762 return "\n".join(cpu_names_str)
763diff --git a/bugit/helper/jira_helper.py b/bugit/helper/jira_helper.py
764new file mode 100644
765index 0000000..1ce6045
766--- /dev/null
767+++ b/bugit/helper/jira_helper.py
768@@ -0,0 +1,151 @@
769+#!/usr/bin/python3
770+
771+import os
772+import configparser
773+
774+from jira import JIRA
775+
776+JIRA_SERVER = "https://warthogs.atlassian.net/"
777+JIRA_CONFIG_DIR = "{}/.jira.conf".format(os.path.expanduser('~'))
778+
779+
780+class JiraAssistant():
781+
782+ def __init__(self, server=JIRA_SERVER, config_dir=JIRA_CONFIG_DIR):
783+ self.server = server
784+ self.config_dir = config_dir
785+
786+ config = configparser.ConfigParser()
787+ while not config.read(self.config_dir):
788+ self.create_token()
789+ jiracfg = config["jira"]
790+ self.user = jiracfg["user"]
791+ self.token = jiracfg["token"]
792+
793+ self.jira = JIRA(server=self.server,
794+ basic_auth=(self.user, self.token))
795+ self.bug = None
796+
797+ def create_token(self):
798+ """ Create token file to store user's email and token
799+ """
800+ email = input("Enter the user's email: ")
801+ token = input(
802+ "Enter the user's token (see "
803+ "https://id.atlassian.com/manage-profile/security/api-tokens): "
804+ )
805+
806+ config = configparser.ConfigParser()
807+ config.add_section("jira")
808+ jiracfg = config["jira"]
809+ jiracfg["user"] = email
810+ jiracfg["token"] = token
811+ with open(self.config_dir, 'w') as configfile:
812+ config.write(configfile)
813+
814+ def get_bug_information(self, bug_id):
815+ """ Get bug's informations by given bug ID
816+ """
817+ try:
818+ print("Checking bug...")
819+ self.bug = self.jira.issue(id=bug_id)
820+ except Exception:
821+ error_msg = f"{bug_id} jira bug not found"
822+ raise JIRAAssistantError(error_msg)
823+
824+ bug_info = {
825+ "title": self.bug.raw['fields']['summary'],
826+ "importance": self.bug.raw['fields']['priority']['name'],
827+ "assignee": self.bug.raw['fields']['assignee']['displayName']
828+ }
829+
830+ return self.bug, bug_info
831+
832+ def check_assignee(self, assignee):
833+ """ Check if the given assignee is exist
834+ """
835+ users = self.jira.search_users(query=assignee)
836+ if len(users) > 1:
837+ error_msg = "There are more than 1 matched users," \
838+ "please give an unique username"
839+ for user in users:
840+ error_msg += f"{user.emailAddress}\n"
841+ raise JIRAAssistantError(error_msg)
842+
843+ elif len(users) == 0:
844+ error_msg = "There is no matched user," \
845+ "please give an unique username"
846+ raise JIRAAssistantError(error_msg)
847+
848+ return users[0].emailAddress
849+
850+ def check_project(self, project_name):
851+ """ Check if the given project key is exist
852+ """
853+ try:
854+ project = self.jira.project(id=project_name)
855+ except Exception:
856+ error_msg = f"Project {project_name} not found on JIRA"
857+ raise JIRAAssistantError(error_msg)
858+
859+ return project.key
860+
861+ def create_bug(self, bug_dict):
862+ """ Create bug to JIRA
863+ """
864+ if bug_dict['assignee']:
865+ print("check assignee...")
866+ assignee_id = self.check_assignee(bug_dict['assignee'])
867+
868+ if bug_dict['project']:
869+ print("check project...")
870+ project_key = self.check_project(bug_dict['project'])
871+
872+ print(f"Creating issue to {project_key}...")
873+ self.bug = self.jira.create_issue(bug_dict)
874+ bug_url = f"https://warthogs.atlassian.net/browse/{self.bug.key}"
875+ print(f"Issue {self.bug.key} is created")
876+
877+ # Assign the ticket to given assignee
878+ self.jira.assign_issue(self.bug.key, assignee_id)
879+
880+ return self.bug, bug_url
881+
882+ def upload_attachments(self, attachments):
883+ """ Upload an attachment to a ticket
884+ """
885+ for a in attachments:
886+ print("Uploading attachment {}...".format(a))
887+ self.jira.add_attachment(self.bug.id,
888+ filename=a,
889+ attachment=attachments[a])
890+
891+ def add_comment(self, comment):
892+ """Add comment to the bug
893+ """
894+ print("Adding comment...")
895+ update_dict = {'comment': [{'add': {'body': comment}}]}
896+ self.bug.update(update=update_dict)
897+
898+ def update_bug(self, bug_dict):
899+ """Update bug informations
900+ """
901+
902+ if bug_dict['assignee']:
903+ print("check assignee...")
904+ assignee_id = self.check_assignee(bug_dict['assignee'])
905+ self.jira.assign_issue(self.bug.key, assignee_id)
906+
907+ update_dict = {"priority": bug_dict['priority']}
908+ self.bug.update(fields=update_dict)
909+
910+ bug_url = f"https://warthogs.atlassian.net/browse/{self.bug.key}"
911+ print(f"Issue {self.bug.key} is updated")
912+
913+ return self.bug, bug_url
914+
915+
916+class JIRAAssistantError(Exception):
917+ """Raised when an error is reported during upload to JIRA."""
918+
919+ pass
920diff --git a/bugit/helper/launchpad_helper.py b/bugit/helper/launchpad_helper.py
921new file mode 100644
922index 0000000..90ca6fc
923--- /dev/null
924+++ b/bugit/helper/launchpad_helper.py
925@@ -0,0 +1,234 @@
926+import os
927+from launchpadlib.launchpad import Launchpad
928+from launchpadlib.uris import LPNET_WEB_ROOT, STAGING_WEB_ROOT, \
929+ QASTAGING_WEB_ROOT
930+
931+
932+class LaunchpadAssistant():
933+ def __init__(self):
934+ # can be 'production', 'staging' or 'qastaging'
935+ service_root = os.environ.get(
936+ "APPORT_LAUNCHPAD_INSTANCE", "production")
937+ print("Using {} service root".format(service_root))
938+
939+ directory = os.path.expanduser("~/.launchpadlib")
940+ if not os.path.isdir(directory):
941+ os.mkdir(directory)
942+
943+ launchpad = Launchpad.login_with(
944+ "bugit",
945+ service_root,
946+ credentials_file=os.path.join(directory, "bugit"),
947+ allow_access_levels=["WRITE_PRIVATE"],
948+ )
949+
950+ # Small trick to force access to launchpad and verify authentication
951+ launchpad.me
952+ self.launchpad = launchpad
953+ self.bug = None
954+
955+ def get_bug_information(self, bug_id):
956+ """ Get existing bug's informations from launchpad
957+
958+ Args:
959+ bug_id (string): bug ID to retrive from launchpad
960+ """
961+
962+ try:
963+ print("Checking bug...")
964+ self.bug = self.launchpad.bugs[bug_id]
965+ except Exception:
966+ error_message = "{} launchpad bug not found".format(bug_id)
967+ raise LaunchpadAssistantError(error_message)
968+
969+ bug_task = self.bug.bug_tasks[0]
970+ bug_info = {
971+ "status": bug_task.status,
972+ "title": bug_task.title,
973+ "tags": " ".join(self.bug.lp_get_parameter("tags")),
974+ "assignee": bug_task.assignee.name if bug_task.assignee else "",
975+ "importance": bug_task.importance
976+ }
977+
978+ return self.bug, bug_info
979+
980+ def check_project_exist(self, project_name):
981+ """ Check if given project name exist
982+ """
983+
984+ try:
985+ print("Checking project name...")
986+ project = self.launchpad.projects[project_name]
987+ except Exception:
988+ error_message = "{} launchpad project not found".format(
989+ project_name)
990+ raise LaunchpadAssistantError(error_message)
991+
992+ return project
993+
994+ def check_series_exist(self, project, series_name):
995+ """ Check if given series name exist in the project
996+ """
997+
998+ print("Checking series...")
999+ series = project.getSeries(name=series_name)
1000+ if series is None:
1001+ error_message = "{} series not found".format(series_name)
1002+ raise LaunchpadAssistantError(error_message)
1003+
1004+ return series
1005+
1006+ def check_assignee_exist(self, assignee_name):
1007+ """ Check if given assignee exist
1008+ """
1009+
1010+ try:
1011+ print("Checking assignee...")
1012+ assignee = self.launchpad.people[assignee_name]
1013+ except Exception:
1014+ error_message = "{} launchpad user not found".format(assignee_name)
1015+ raise LaunchpadAssistantError(error_message)
1016+
1017+ return assignee
1018+
1019+ def create_bug(self, bug_dict):
1020+ """Create issue to Launchpad with given informations
1021+
1022+ Args:
1023+ bug_dict (dict): Contain all information needed to creating a bug
1024+
1025+ """
1026+ # checking if the project is exist
1027+ lp_project = None
1028+ lp_project = self.check_project_exist(bug_dict['project'])
1029+
1030+ # checking if the series is exist
1031+ lp_series = None
1032+ if bug_dict['series']:
1033+ lp_series = self.check_series_exist(lp_project, bug_dict['series'])
1034+
1035+ # checking if assignee is exist
1036+ lp_assignee = None
1037+ if bug_dict['assignee']:
1038+ lp_assignee = self.check_assignee_exist(bug_dict['assignee'])
1039+
1040+ if not self.bug:
1041+ print("Creating Launchpad bug report...")
1042+ self.bug = self.launchpad.bugs.createBug(
1043+ title=bug_dict['title'],
1044+ description=bug_dict['description'],
1045+ tags=bug_dict['tags'].split(),
1046+ target=lp_project
1047+ )
1048+ print("Bug report #{} created.".format(self.bug.id))
1049+ else:
1050+ print("Updating Launchpad bug report...")
1051+ self.bug.title = self.lp_title
1052+ self.bug.description = self.lp_description
1053+ self.bug.tags = self.lp_tags.split()
1054+ self.bug.lp_save()
1055+
1056+ # Task configuration
1057+ task = self.bug.bug_tasks[0]
1058+ if self.bug and task.target != lp_project:
1059+ print("Updating project...")
1060+ task.target = lp_project
1061+ task.lp_save()
1062+
1063+ if lp_series:
1064+ print("Setting series...")
1065+ nomination = self.bug.addNomination(target=lp_series)
1066+ nomination.approve()
1067+
1068+ # We update bug info only for the latest created series
1069+ task = self.bug.bug_tasks[len(self.bug.bug_tasks) - 1]
1070+ if lp_assignee:
1071+ print(f"Setting assignee for series {task.bug_target_name}...")
1072+ task.assignee = lp_assignee
1073+ print("Setting status...")
1074+ task.status = bug_dict['status']
1075+ print("Setting importance...")
1076+ task.importance = bug_dict['priority']
1077+
1078+ task.lp_save()
1079+
1080+ service_root = os.environ.get(
1081+ "APPORT_LAUNCHPAD_INSTANCE", "production")
1082+ if service_root == "qastaging":
1083+ bug_url = QASTAGING_WEB_ROOT + f"bugs/{self.bug.id}"
1084+ elif service_root == "staging":
1085+ bug_url = STAGING_WEB_ROOT + f"bugs/{self.bug.id}"
1086+ else:
1087+ bug_url = LPNET_WEB_ROOT + f"bugs/{self.bug.id}"
1088+
1089+ print("Bug report #{} updated.".format(self.bug.id))
1090+ print(bug_url)
1091+
1092+ return self.bug, bug_url
1093+
1094+ def upload_attachments(self, attachments):
1095+ """Upload attachments to the issue
1096+
1097+ Args:
1098+ attachments (list): List of attachments
1099+ """
1100+
1101+ for a in attachments:
1102+ print("Uploading attachment {}...".format(a))
1103+ # bug = self.launchpad.bugs[bug_id]
1104+ self.bug.addAttachment(
1105+ comment="Automatically attached",
1106+ filename=a,
1107+ data=attachments[a]
1108+ )
1109+
1110+ def add_comment(self, comment):
1111+ """Add comment to the bug
1112+ """
1113+
1114+ print("Adding comment...")
1115+ self.bug.newMessage(content=comment)
1116+
1117+ def update_bug(self, bug_dict):
1118+ """Upload bug informations
1119+ """
1120+
1121+ bug_task = self.bug.bug_tasks[0]
1122+
1123+ lp_assignee = None
1124+ if bug_dict['assignee']:
1125+ lp_assignee = self.check_assignee_exist(bug_dict['assignee'])
1126+ print("Setting assignee to {}...".format(bug_dict['assignee']))
1127+ bug_task.assignee = lp_assignee
1128+
1129+ if bug_dict['status']:
1130+ print("Setting status to {}...".format(bug_dict['status']))
1131+ bug_task.status = bug_dict['status']
1132+ if bug_dict['priority']:
1133+ print("Setting priority to {}...".format(bug_dict['priority']))
1134+ bug_task.importance = bug_dict['priority']
1135+ if bug_dict['tags']:
1136+ print("Setting tag...")
1137+ self.bug.tags = bug_dict['tags'].split()
1138+
1139+ self.bug.lp_save()
1140+ bug_task.lp_save()
1141+
1142+ service_root = os.environ.get(
1143+ "APPORT_LAUNCHPAD_INSTANCE", "production")
1144+ if service_root == "qastaging":
1145+ bug_url = QASTAGING_WEB_ROOT + f"bugs/{self.bug.id}"
1146+ elif service_root == "staging":
1147+ bug_url = STAGING_WEB_ROOT + f"bugs/{self.bug.id}"
1148+ else:
1149+ bug_url = LPNET_WEB_ROOT + f"bugs/{self.bug.id}"
1150+
1151+ print("Bug report #{} updated.".format(self.bug.id))
1152+
1153+ return self.bug, bug_url
1154+
1155+
1156+class LaunchpadAssistantError(Exception):
1157+ """Raised when an error is reported during upload to Launchpad."""
1158+
1159+ pass
1160diff --git a/bugit/ui.py b/bugit/ui.py
1161index cdd0268..986621d 100644
1162--- a/bugit/ui.py
1163+++ b/bugit/ui.py
1164@@ -127,8 +127,10 @@ class ReportScreen:
1165 ]
1166
1167 statuses = ("New", "Confirmed")
1168- importances = ("Undecided", "Wishlist", "Low", "Medium", "High", "Critical")
1169-
1170+ importances = {
1171+ 'launchpad': ("Undecided", "Wishlist", "Low", "Medium", "High", "Critical"),
1172+ 'jira': ("Lowest", "Low", "Medium", "High", "Highest")
1173+ }
1174 STAGE_IMMEDIATE = "Issue reported and logs collected right after it happened"
1175 STAGE_FROZEN_DEVICE = (
1176 "Device froze, issue reported and logs collected right after a reboot"
1177@@ -162,6 +164,7 @@ class ReportScreen:
1178 cid="",
1179 job_id="",
1180 job_output=None,
1181+ template="Launchpad",
1182 ):
1183 if job_id and job_output:
1184 self.default_description += f"[Checkbox job `{job_id}` output]\n\n"
1185@@ -202,7 +205,7 @@ class ReportScreen:
1186 for s in self.statuses:
1187 rb = urwid.RadioButton(self._status, s, "first True", None)
1188 self._importance = []
1189- for i in self.importances:
1190+ for i in self.importances[template]:
1191 rb = urwid.RadioButton(self._importance, i, "first True", None)
1192 self._stage_bug_filed = []
1193 for s in self.stage_bug_filed:
1194@@ -226,7 +229,7 @@ class ReportScreen:
1195 )
1196 self._bug_vendors.append(cb)
1197 self._assignee = urwid.LineBox(
1198- urwid.Edit(edit_text=assignee), "Assigned To (Launchpad ID)"
1199+ urwid.Edit(edit_text=assignee), "Assigned To"
1200 )
1201 self._tags = urwid.LineBox(urwid.Edit(edit_text=tags), "Tags")
1202 status_list = urwid.LineBox(urwid.Pile(self._status), "Status")
1203@@ -377,6 +380,7 @@ class ReopenReportScreen(ReportScreen):
1204 importance="",
1205 job_id="",
1206 job_output=None,
1207+ template="Launchpad"
1208 ):
1209 if job_id and job_output:
1210 job_pattern = ["comments", "stdout", "stderr"]
1211@@ -417,7 +421,7 @@ class ReopenReportScreen(ReportScreen):
1212 self._status, self.statuses[1], "first True", None
1213 )
1214 self._importance = []
1215- for i in self.importances:
1216+ for i in self.importances[template]:
1217 rb = urwid.RadioButton(self._importance, i, "first True", None)
1218 [rb.set_state(True) for rb in self._importance
1219 if rb.label == importance]

Subscribers

People subscribed via source and target branches

to all changes: