mgr/dashboard: report ceph tracker bug/feature through CLI/API

Fixes: https://tracker.ceph.com/issues/44851

Signed-off-by: Shreya Sharma <shreyasharma.ss305@gmail.com>
This commit is contained in:
cypherean 2021-08-24 15:25:42 +05:30
parent 384c2f04c5
commit e90ebc195e
8 changed files with 362 additions and 2 deletions

View File

@ -1458,3 +1458,44 @@ something like this::
...
$ ceph config reset 11
Reporting issues from Dashboard
"""""""""""""""""""""""""""""""
Ceph-Dashboard provides two ways to create an issue in the Ceph Issue Tracker,
either using the Ceph command line interface or by using the Ceph Dashboard
user interface.
To create an issue in the Ceph Issue Tracker, a user needs to have an account
on the issue tracker. Under the ``my account`` tab in the Ceph Issue Tracker,
the user can see their API access key. This key is used for authentication
when creating a new issue. To store the Ceph API access key, in the CLI run:
``ceph dashboard set-issue-tracker-api-key -i <file-containing-key>``
Then on successful update, you can create an issue using:
``ceph dashboard create issue <project> <tracker_type> <subject> <description>``
The available projects to create an issue on are:
#. dashboard
#. block
#. object
#. file_system
#. ceph_manager
#. orchestrator
#. ceph_volume
#. core_ceph
The available tracker types are:
#. bug
#. feature
The subject and description are then set by the user.
The user can also create an issue using the Dashboard user interface. The settings
icon drop down menu on the top right of the navigation bar has the option to
``Raise an issue``. On clicking it, a modal dialog opens that has the option to
select the project and tracker from their respective drop down menus. The subject
and multiline description are added by the user. The user can then submit the issue.

View File

@ -0,0 +1,60 @@
# # -*- coding: utf-8 -*-
from ..exceptions import DashboardException
from ..model.feedback import Feedback
from ..rest_client import RequestException
from ..security import Scope
from ..services import feedback
from . import ApiController, ControllerDoc, RESTController
@ApiController('/feedback', Scope.CONFIG_OPT)
@ControllerDoc("Feedback API", "Report")
class FeedbackController(RESTController):
issueAPIkey = None
def __init__(self): # pragma: no cover
super(FeedbackController, self).__init__()
self.tracker_client = feedback.CephTrackerClient()
@RESTController.MethodMap(version='0.1')
def create(self, project, tracker, subject, description):
"""
Create an issue.
:param project: The affected ceph component.
:param tracker: The tracker type.
:param subject: The title of the issue.
:param description: The description of the issue.
"""
try:
new_issue = Feedback(Feedback.Project[project].value,
Feedback.TrackerType[tracker].value, subject, description)
except KeyError:
raise DashboardException(msg=f'{"Invalid arguments"}', component='feedback')
try:
return self.tracker_client.create_issue(new_issue)
except RequestException as error:
if error.status_code == 401:
raise DashboardException(msg=f'{"Invalid API key"}',
http_status_code=error.status_code,
component='feedback')
raise error
except Exception:
raise DashboardException(msg=f'{"API key not set"}',
http_status_code=401,
component='feedback')
@RESTController.MethodMap(version='0.1')
def get(self, issue_number):
"""
Fetch issue details.
:param issueAPI: The issue tracker API access key.
"""
try:
return self.tracker_client.get_issues(issue_number)
except RequestException as error:
if error.status_code == 404:
raise DashboardException(msg=f'Issue {issue_number} not found',
http_status_code=error.status_code,
component='feedback')
raise error

View File

@ -0,0 +1 @@
# -*- coding: utf-8 -*-

View File

@ -0,0 +1,47 @@
# # -*- coding: utf-8 -*-
from enum import Enum
class Feedback:
project_id: int
tracker_id: int
subject: str
description: str
status: int
class Project(Enum):
dashboard = 46
block = 9 # rbd
object = 10 # rgw
file_system = 13 # cephfs
ceph_manager = 46
orchestrator = 42
ceph_volume = 39
core_ceph = 36 # rados
class TrackerType(Enum):
bug = 1
feature = 2
class Status(Enum):
new = 1
def __init__(self, project_id, tracker_id, subject, description):
self.project_id = int(project_id)
self.tracker_id = int(tracker_id)
self.subject = subject
self.description = description
self.status = Feedback.Status.new.value
def as_dict(self):
return {
"issue": {
"project": {
"id": self.project_id
},
"tracker_id": self.tracker_id,
"Status": self.status,
"subject": self.subject,
"description": self.description
}
}

View File

@ -19,16 +19,19 @@ if TYPE_CHECKING:
else:
from typing_extensions import Literal
from mgr_module import CLIWriteCommand, HandleCommandResult, MgrModule, \
MgrStandbyModule, Option, _get_localized_key
from mgr_module import CLICommand, CLIWriteCommand, HandleCommandResult, \
MgrModule, MgrStandbyModule, Option, _get_localized_key
from mgr_util import ServerConfigException, build_url, \
create_self_signed_cert, get_default_addr, verify_tls_files
from . import mgr
from .controllers import generate_routes, json_error_page
from .grafana import push_local_dashboards
from .model.feedback import Feedback
from .rest_client import RequestException
from .services.auth import AuthManager, AuthManagerTool, JwtManager
from .services.exception import dashboard_exception_handler
from .services.feedback import CephTrackerClient
from .services.rgw_client import configure_rgw_credentials
from .services.sso import SSO_COMMANDS, handle_sso_command
from .settings import handle_option_command, options_command_list, options_schema_list
@ -406,6 +409,45 @@ class Module(MgrModule, CherryPyConfig):
return result
return 0, 'Self-signed certificate created', ''
@CLICommand("dashboard get issue")
def get_issues_cli(self, issue_number: int):
try:
issue_number = int(issue_number)
except TypeError:
return -errno.EINVAL, '', f'Invalid issue number {issue_number}'
tracker_client = CephTrackerClient()
try:
response = tracker_client.get_issues(issue_number)
except RequestException as error:
if error.status_code == 404:
return -errno.EINVAL, '', f'Issue {issue_number} not found'
else:
return -errno.EREMOTEIO, '', f'Error: {str(error)}'
return 0, str(response), ''
@CLICommand("dashboard create issue")
def report_issues_cli(self, project: str, tracker: str, subject: str, description: str):
'''
Create an issue in the Ceph Issue tracker
Syntax: ceph dashboard create issue <project> <bug|feature> <subject> <description>
'''
try:
feedback = Feedback(Feedback.Project[project].value,
Feedback.TrackerType[tracker].value, subject, description)
except KeyError:
return -errno.EINVAL, '', 'Invalid arguments'
tracker_client = CephTrackerClient()
try:
response = tracker_client.create_issue(feedback)
except RequestException as error:
if error.status_code == 401:
return -errno.EINVAL, '', 'Invalid API Key'
else:
return -errno.EINVAL, '', f'Error: {str(error)}'
except Exception:
return -errno.EINVAL, '', 'Ceph Tracker API key not set'
return 0, str(response), ''
@CLIWriteCommand("dashboard set-rgw-credentials")
def set_rgw_credentials(self):
try:

View File

@ -2644,6 +2644,85 @@ paths:
summary: Get List Of Features
tags:
- FeatureTogglesEndpoint
/api/feedback:
post:
description: "\n Create an issue.\n :param project: The affected\
\ ceph component.\n :param tracker: The tracker type.\n :param\
\ subject: The title of the issue.\n :param description: The description\
\ of the issue.\n "
parameters: []
requestBody:
content:
application/json:
schema:
properties:
description:
type: string
project:
type: string
subject:
type: string
tracker:
type: string
required:
- project
- tracker
- subject
- description
type: object
responses:
'201':
content:
application/vnd.ceph.api.v0.1+json:
type: object
description: Resource created.
'202':
content:
application/vnd.ceph.api.v0.1+json:
type: object
description: Operation is still executing. Please check the task queue.
'400':
description: Operation exception. Please check the response body for details.
'401':
description: Unauthenticated access. Please login first.
'403':
description: Unauthorized access. Please check your permissions.
'500':
description: Unexpected error. Please check the response body for the stack
trace.
security:
- jwt: []
tags:
- Report
/api/feedback/{issue_number}:
get:
description: "\n Fetch issue details.\n :param issueAPI: The issue\
\ tracker API access key.\n "
parameters:
- in: path
name: issue_number
required: true
schema:
type: integer
responses:
'200':
content:
application/vnd.ceph.api.v0.1+json:
type: object
description: OK
'400':
description: Operation exception. Please check the response body for details.
'401':
description: Unauthenticated access. Please login first.
'403':
description: Unauthorized access. Please check your permissions.
'500':
description: Unexpected error. Please check the response body for the stack
trace.
security:
- jwt: []
tags:
- Report
/api/grafana/dashboards:
post:
parameters: []
@ -10361,6 +10440,8 @@ tags:
name: RbdSnapshot
- description: RBD Trash Management API
name: RbdTrash
- description: Feedback API
name: Report
- description: RGW Management API
name: Rgw
- description: RGW Bucket Management API

View File

@ -0,0 +1,85 @@
# -*- coding: utf-8 -*-
import json
import requests
from requests.auth import AuthBase
from ..model.feedback import Feedback
from ..rest_client import RequestException, RestClient
from ..settings import Settings
class config:
url = 'tracker.ceph.com'
port = 443
class RedmineAuth(AuthBase):
def __init__(self):
try:
self.access_key = Settings.ISSUE_TRACKER_API_KEY
except KeyError:
self.access_key = None
def __call__(self, r):
r.headers['X-Redmine-API-Key'] = self.access_key
return r
class CephTrackerClient(RestClient):
access_key = ''
def __init__(self):
super().__init__(config.url, config.port, client_name='CephTracker',
ssl=True, auth=RedmineAuth(), ssl_verify=True)
@staticmethod
def get_api_key():
try:
access_key = Settings.ISSUE_TRACKER_API_KEY
except KeyError:
raise KeyError("Key not set")
if access_key == '':
raise KeyError("Empty key")
return access_key
def get_issues(self, issue_number):
'''
Fetch an issue from the Ceph Issue tracker
'''
headers = {
'Content-Type': 'application/json',
}
response = requests.get(
f'https://tracker.ceph.com/issues/{issue_number}.json', headers=headers)
if not response.ok:
raise RequestException(
"Request failed with status code {}\n"
.format(response.status_code),
self._handle_response_status_code(response.status_code),
response.content)
return {"message": response.text}
def create_issue(self, feedback: Feedback):
'''
Create an issue in the Ceph Issue tracker
'''
try:
headers = {
'Content-Type': 'application/json',
'X-Redmine-API-Key': self.get_api_key(),
}
except KeyError:
raise Exception("Ceph Tracker API Key not set")
data = json.dumps(feedback.as_dict())
response = requests.post(
f'https://tracker.ceph.com/projects/{feedback.project_id}/issues.json',
headers=headers, data=data)
if not response.ok:
raise RequestException(
"Request failed with status code {}\n"
.format(response.status_code),
self._handle_response_status_code(response.status_code),
response.content)
return {"message": response.text}

View File

@ -68,6 +68,9 @@ class Options(object):
RGW_API_ADMIN_RESOURCE = Setting('admin', [str])
RGW_API_SSL_VERIFY = Setting(True, [bool])
# Ceph Issue Tracker API Access Key
ISSUE_TRACKER_API_KEY = Setting('', [str])
# Grafana settings
GRAFANA_API_URL = Setting('', [str])
GRAFANA_FRONTEND_API_URL = Setting('', [str])