From: cypherean Date: Tue, 24 Aug 2021 09:55:42 +0000 (+0530) Subject: mgr/dashboard: report ceph tracker bug/feature through CLI/API X-Git-Tag: v17.1.0~1020^2 X-Git-Url: http://git-server-git.apps.pok.os.sepia.ceph.com/?a=commitdiff_plain;h=refs%2Fpull%2F42090%2Fhead;p=ceph.git mgr/dashboard: report ceph tracker bug/feature through CLI/API Fixes: https://tracker.ceph.com/issues/44851 Signed-off-by: Shreya Sharma --- diff --git a/doc/mgr/dashboard.rst b/doc/mgr/dashboard.rst index abc8cf9eee5d..73764ea86df5 100644 --- a/doc/mgr/dashboard.rst +++ b/doc/mgr/dashboard.rst @@ -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 `` + +Then on successful update, you can create an issue using: + +``ceph dashboard create issue `` + +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. + diff --git a/src/pybind/mgr/dashboard/controllers/feedback.py b/src/pybind/mgr/dashboard/controllers/feedback.py new file mode 100644 index 000000000000..99c14086476b --- /dev/null +++ b/src/pybind/mgr/dashboard/controllers/feedback.py @@ -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 diff --git a/src/pybind/mgr/dashboard/model/__init__.py b/src/pybind/mgr/dashboard/model/__init__.py new file mode 100644 index 000000000000..40a96afc6ff0 --- /dev/null +++ b/src/pybind/mgr/dashboard/model/__init__.py @@ -0,0 +1 @@ +# -*- coding: utf-8 -*- diff --git a/src/pybind/mgr/dashboard/model/feedback.py b/src/pybind/mgr/dashboard/model/feedback.py new file mode 100644 index 000000000000..902f18256378 --- /dev/null +++ b/src/pybind/mgr/dashboard/model/feedback.py @@ -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 + } + } diff --git a/src/pybind/mgr/dashboard/module.py b/src/pybind/mgr/dashboard/module.py index dc1beb8d9826..9d389eb25740 100644 --- a/src/pybind/mgr/dashboard/module.py +++ b/src/pybind/mgr/dashboard/module.py @@ -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 + ''' + 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: diff --git a/src/pybind/mgr/dashboard/openapi.yaml b/src/pybind/mgr/dashboard/openapi.yaml index ff2dea0e99a3..e30cb5dfac08 100644 --- a/src/pybind/mgr/dashboard/openapi.yaml +++ b/src/pybind/mgr/dashboard/openapi.yaml @@ -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 diff --git a/src/pybind/mgr/dashboard/services/feedback.py b/src/pybind/mgr/dashboard/services/feedback.py new file mode 100644 index 000000000000..7e4c7ad70a9e --- /dev/null +++ b/src/pybind/mgr/dashboard/services/feedback.py @@ -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} diff --git a/src/pybind/mgr/dashboard/settings.py b/src/pybind/mgr/dashboard/settings.py index 72a0f815a655..6018f0d7f9c7 100644 --- a/src/pybind/mgr/dashboard/settings.py +++ b/src/pybind/mgr/dashboard/settings.py @@ -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])