]> git-server-git.apps.pok.os.sepia.ceph.com Git - ceph.git/commitdiff
mgr/dashboard: report ceph tracker bug/feature through CLI/API 42090/head
authorcypherean <shreyasharma.ss305@gmail.com>
Tue, 24 Aug 2021 09:55:42 +0000 (15:25 +0530)
committercypherean <shreyasharma.ss305@gmail.com>
Tue, 24 Aug 2021 17:07:52 +0000 (22:37 +0530)
Fixes: https://tracker.ceph.com/issues/44851
Signed-off-by: Shreya Sharma <shreyasharma.ss305@gmail.com>
doc/mgr/dashboard.rst
src/pybind/mgr/dashboard/controllers/feedback.py [new file with mode: 0644]
src/pybind/mgr/dashboard/model/__init__.py [new file with mode: 0644]
src/pybind/mgr/dashboard/model/feedback.py [new file with mode: 0644]
src/pybind/mgr/dashboard/module.py
src/pybind/mgr/dashboard/openapi.yaml
src/pybind/mgr/dashboard/services/feedback.py [new file with mode: 0644]
src/pybind/mgr/dashboard/settings.py

index abc8cf9eee5dc3027f05676f65bc7d49190274a1..73764ea86df5bf62ab238c1148c4f8a134caa003 100644 (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.
+
diff --git a/src/pybind/mgr/dashboard/controllers/feedback.py b/src/pybind/mgr/dashboard/controllers/feedback.py
new file mode 100644 (file)
index 0000000..99c1408
--- /dev/null
@@ -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 (file)
index 0000000..40a96af
--- /dev/null
@@ -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 (file)
index 0000000..902f182
--- /dev/null
@@ -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
+            }
+        }
index dc1beb8d9826404830752d10eec801d05a15d400..9d389eb257401f6f85bfe6736cc238a2e4618047 100644 (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:
index ff2dea0e99a38b5ee86156305898a27c3f35fc5f..e30cb5dfac08d6f09d2490c718ae06b0a9042b3c 100644 (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
diff --git a/src/pybind/mgr/dashboard/services/feedback.py b/src/pybind/mgr/dashboard/services/feedback.py
new file mode 100644 (file)
index 0000000..7e4c7ad
--- /dev/null
@@ -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}
index 72a0f815a6552d39a081515b5ebdb31ff5641408..6018f0d7f9c73facc52097e0efe1db1df6e33ef3 100644 (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])