]> git.apps.os.sepia.ceph.com Git - ceph.git/commitdiff
mgr/dashboard: report ceph tracker bug/feature through GUI 42603/head
authorAvan Thakkar <athakkar@redhat.com>
Mon, 10 Jan 2022 09:22:26 +0000 (14:52 +0530)
committerAvan Thakkar <athakkar@redhat.com>
Mon, 17 Jan 2022 14:15:31 +0000 (19:45 +0530)
Fixes: https://tracker.ceph.com/issues/44851
Signed-off-by: Shreya Sharma <shreyasharma.ss305@gmail.com>
Signed-off-by: Avan Thakkar <athakkar@redhat.com>
20 files changed:
qa/tasks/mgr/dashboard/test_feedback.py [new file with mode: 0644]
src/mon/MgrMonitor.cc
src/pybind/mgr/dashboard/controllers/feedback.py
src/pybind/mgr/dashboard/frontend/src/app/ceph/dashboard/dashboard.module.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/shared/feedback/feedback.component.html [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/shared/feedback/feedback.component.scss [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/shared/feedback/feedback.component.spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/shared/feedback/feedback.component.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/core/navigation/dashboard-help/dashboard-help.component.html
src/pybind/mgr/dashboard/frontend/src/app/core/navigation/dashboard-help/dashboard-help.component.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/api/feedback.service.spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/shared/api/feedback.service.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/model/feedback.py [deleted file]
src/pybind/mgr/dashboard/module.py
src/pybind/mgr/dashboard/openapi.yaml
src/pybind/mgr/dashboard/services/feedback.py [deleted file]
src/pybind/mgr/feedback/__init__.py [new file with mode: 0644]
src/pybind/mgr/feedback/model.py [new file with mode: 0644]
src/pybind/mgr/feedback/module.py [new file with mode: 0644]
src/pybind/mgr/feedback/service.py [new file with mode: 0644]

diff --git a/qa/tasks/mgr/dashboard/test_feedback.py b/qa/tasks/mgr/dashboard/test_feedback.py
new file mode 100644 (file)
index 0000000..0ec5ac3
--- /dev/null
@@ -0,0 +1,36 @@
+import time
+
+from .helper import DashboardTestCase
+
+
+class FeedbackTest(DashboardTestCase):
+
+    @classmethod
+    def setUpClass(cls):
+        super().setUpClass()
+        cls._ceph_cmd(['mgr', 'module', 'enable', 'feedback'])
+        time.sleep(10)
+
+    def test_create_api_key(self):
+        self._post('/api/feedback/api_key', {'api_key': 'testapikey'}, version='0.1')
+        self.assertStatus(201)
+
+    def test_get_api_key(self):
+        response = self._get('/api/feedback/api_key', version='0.1')
+        self.assertStatus(200)
+        self.assertEqual(response, 'testapikey')
+
+    def test_remove_api_key(self):
+        self._delete('/api/feedback/api_key', version='0.1')
+        self.assertStatus(204)
+
+    def test_issue_tracker_create_with_invalid_key(self):
+        self._post('/api/feedback', {'api_key': 'invalidapikey', 'description': 'test',
+                                     'project': 'dashboard', 'subject': 'sub', 'tracker': 'bug'},
+                   version='0.1')
+        self.assertStatus(400)
+
+    def test_issue_tracker_create_with_invalid_params(self):
+        self._post('/api/feedback', {'api_key': '', 'description': 'test', 'project': 'xyz',
+                                     'subject': 'testsub', 'tracker': 'invalid'}, version='0.1')
+        self.assertStatus(400)
index ed44af17d0203b2c3adf2dee47daa4cda3aaa35c..2b1b5cba3c1962c82cd4b35c789f68fbdd17764a 100644 (file)
@@ -106,6 +106,7 @@ const static std::map<uint32_t, std::set<std::string>> always_on_modules = {
       "progress",
       "balancer",
       "devicehealth",
+      "feedback",
       "orchestrator",
       "rbd_support",
       "volumes",
index e1a9eb31d6c20a30948dd3e3ba9bee03eb5495ff..3b3474d39ea47cdbe1240f42db64f4302f0ee656 100644 (file)
 # # -*- coding: utf-8 -*-
 
+from .. import mgr
 from ..exceptions import DashboardException
-from ..model.feedback import Feedback
-from ..rest_client import RequestException
 from ..security import Scope
-from ..services import feedback
-from . import APIDoc, APIRouter, RESTController
+from . import APIDoc, APIRouter, BaseController, Endpoint, ReadPermission, RESTController, UIRouter
+from ._version import APIVersion
 
 
 @APIRouter('/feedback', Scope.CONFIG_OPT)
-@APIDoc("Feedback API", "Report")
+@APIDoc("Feedback", "Report")
 class FeedbackController(RESTController):
-    issueAPIkey = None
 
-    def __init__(self):  # pragma: no cover
-        super().__init__()
-        self.tracker_client = feedback.CephTrackerClient()
+    @RESTController.MethodMap(version=APIVersion.EXPERIMENTAL)
+    def list(self):
+        """
+        List all issues details.
+        """
+        try:
+            response = mgr.remote('feedback', 'get_issues')
+        except RuntimeError as error:
+            raise DashboardException(msg=f'Error in fetching issue list: {str(error)}',
+                                         http_status_code=error.status_code,
+                                         component='feedback')
+        return response
 
-    def create(self, project, tracker, subject, description):
+    @RESTController.MethodMap(version=APIVersion.EXPERIMENTAL)
+    def create(self, project, tracker, subject, description, api_key=None):
         """
         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.
+        :param api_key: Ceph tracker api key.
         """
         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,
+            response = mgr.remote('feedback', 'validate_and_create_issue',
+                                  project, tracker, subject, description, api_key)
+        except RuntimeError as error:
+            if "Invalid issue tracker API key" in str(error):
+                raise DashboardException(msg='Error in creating tracker issue: Invalid API key',
+                                         component='feedback')
+            if "KeyError" in str(error):
+                raise DashboardException(msg=f'Error in creating tracker issue: {error}',
                                          component='feedback')
-            raise error
-        except Exception:
-            raise DashboardException(msg=f'{"API key not set"}',
-                                     http_status_code=401,
+            raise DashboardException(msg=f'{error}',
+                                     http_status_code=500,
                                      component='feedback')
 
-    def get(self, issue_number):
+        return response
+
+
+@APIRouter('/feedback/api_key', Scope.CONFIG_OPT)
+@APIDoc("Feedback API", "Report")
+class FeedbackApiController(RESTController):
+
+    @RESTController.MethodMap(version=APIVersion.EXPERIMENTAL)
+    def list(self):
         """
-        Fetch issue details.
-        :param issueAPI: The issue tracker API access key.
+        Returns Ceph tracker API 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
+            api_key = mgr.remote('feedback', 'get_api_key')
+        except ImportError:
+            raise DashboardException(msg='Feedback module not found.',
+                                     http_status_code=404,
+                                     component='feedback')
+        except RuntimeError as error:
+            raise DashboardException(msg=f'{error}',
+                                     http_status_code=500,
+                                     component='feedback')
+        if api_key is None:
+            raise DashboardException(msg='Issue tracker API key is not set',
+                                     component='feedback')
+        return api_key
+
+    @RESTController.MethodMap(version=APIVersion.EXPERIMENTAL)
+    def create(self, api_key):
+        """
+        Sets Ceph tracker API key.
+        :param api_key: The Ceph tracker API key.
+        """
+        try:
+            response = mgr.remote('feedback', 'set_api_key', api_key)
+        except RuntimeError as error:
+            raise DashboardException(msg=f'{error}',
+                                     component='feedback')
+        return response
+
+    @RESTController.MethodMap(version=APIVersion.EXPERIMENTAL)
+    def bulk_delete(self):
+        """
+        Deletes Ceph tracker API key.
+        """
+        try:
+            response = mgr.remote('feedback', 'delete_api_key')
+        except RuntimeError as error:
+            raise DashboardException(msg=f'{error}',
+                                     http_status_code=500,
+                                     component='feedback')
+        return response
+
+
+@UIRouter('/feedback/api_key', Scope.CONFIG_OPT)
+class FeedbackUiController(BaseController):
+    @Endpoint()
+    @ReadPermission
+    def exist(self):
+        """
+        Checks if Ceph tracker API key is stored.
+        """
+        try:
+            response = mgr.remote('feedback', 'is_api_key_set')
+        except RuntimeError:
+            raise DashboardException(msg='Feedback module is not enabled',
+                                     http_status_code=404,
+                                     component='feedback')
+
+        return response
index 1205de94dd6e00a41df6e947e7c715a7ce0215ec..4bdfd50a51bfd25d5125c89acbae7811303e85d5 100644 (file)
@@ -1,5 +1,6 @@
 import { CommonModule } from '@angular/common';
 import { NgModule } from '@angular/core';
+import { FormsModule, ReactiveFormsModule } from '@angular/forms';
 import { RouterModule } from '@angular/router';
 
 import { NgbNavModule, NgbPopoverModule } from '@ng-bootstrap/ng-bootstrap';
@@ -7,6 +8,7 @@ import { ChartsModule } from 'ng2-charts';
 
 import { SharedModule } from '~/app/shared/shared.module';
 import { CephSharedModule } from '../shared/ceph-shared.module';
+import { FeedbackComponent } from '../shared/feedback/feedback.component';
 import { DashboardComponent } from './dashboard/dashboard.component';
 import { HealthPieComponent } from './health-pie/health-pie.component';
 import { HealthComponent } from './health/health.component';
@@ -25,7 +27,9 @@ import { OsdSummaryPipe } from './osd-summary.pipe';
     SharedModule,
     ChartsModule,
     RouterModule,
-    NgbPopoverModule
+    NgbPopoverModule,
+    FormsModule,
+    ReactiveFormsModule
   ],
 
   declarations: [
@@ -37,7 +41,8 @@ import { OsdSummaryPipe } from './osd-summary.pipe';
     MdsSummaryPipe,
     HealthPieComponent,
     InfoCardComponent,
-    InfoGroupComponent
+    InfoGroupComponent,
+    FeedbackComponent
   ]
 })
 export class DashboardModule {}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/shared/feedback/feedback.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/shared/feedback/feedback.component.html
new file mode 100644 (file)
index 0000000..57050d0
--- /dev/null
@@ -0,0 +1,120 @@
+<cd-modal [modalRef]="activeModal">
+  <div class="modal-title"
+       i18n>Report an issue</div>
+
+  <div class="modal-content">
+    <form name="feedbackForm"
+          [formGroup]="feedbackForm"
+          #formDir="ngForm">
+      <div class="modal-body">
+        <cd-alert-panel *ngIf="!isFeedbackEnabled"
+                        type="error"
+                        i18n>Feedback module is not enabled. Please enable it from <a (click)="redirect()">Cluster-> Manager Modules.</a>
+        </cd-alert-panel>
+        <!-- api_key -->
+        <div class="form-group row"
+             *ngIf="!isAPIKeySet">
+          <label class="cd-col-form-label required"
+                 for="api_key"
+                 i18n>Ceph Tracker API Key</label>
+          <div class="cd-col-form-input">
+            <input id="api_key"
+                   type="password"
+                   formControlName="api_key"
+                   class="form-control"
+                   placeholder="Add Ceph tracker API key">
+            <span class="invalid-feedback"
+                  *ngIf="feedbackForm.showError('api_key', formDir, 'required')"
+                  i18n>Ceph Tracker API key is required.</span>
+            <span class="invalid-feedback"
+                  *ngIf="feedbackForm.showError('api_key', formDir, 'invalidApiKey')"
+                  i18n>Ceph Tracker API key is invalid.</span>
+          </div>
+        </div>
+
+        <!-- project -->
+        <div class="form-group row">
+          <label class="cd-col-form-label required"
+                 for="project"
+                 i18n>Project name</label>
+          <div class="cd-col-form-input">
+            <select class="form-control custom-select"
+                    id="project"
+                    formControlName="project">
+              <option ngValue=""
+                      i18n>-- Select a project --</option>
+              <option *ngFor="let projectName of project"
+                      [value]="projectName">{{ projectName }}</option>
+            </select>
+            <span class="invalid-feedback"
+                  *ngIf="feedbackForm.showError('project', formDir, 'required')"
+                  i18n>Project name is required.</span>
+          </div>
+        </div>
+
+        <!-- tracker -->
+        <div class="form-group row">
+          <label class="cd-col-form-label required"
+                 for="tracker"
+                 i18n>Tracker</label>
+          <div class="cd-col-form-input">
+            <select class="form-control custom-select"
+                    id="tracker"
+                    formControlName="tracker">
+              <option ngValue=""
+                      i18n>-- Select a tracker --</option>
+              <option *ngFor="let trackerName of tracker"
+                      [value]="trackerName">{{ trackerName }}</option>
+            </select>
+            <span  class="invalid-feedback"
+                   *ngIf="feedbackForm.showError('tracker', formDir, 'required')"
+                   i18n>Tracker name is required.</span>
+          </div>
+        </div>
+
+        <!-- subject -->
+        <div class="form-group row">
+          <label class="cd-col-form-label required"
+                 for="subject"
+                 i18n>Subject</label>
+          <div class="cd-col-form-input">
+            <input id="subject"
+                   type="text"
+                   formControlName="subject"
+                   class="form-control"
+                   placeholder="Add issue title">
+            <span class="invalid-feedback"
+                  *ngIf="feedbackForm.showError('subject', formDir, 'required')"
+                  i18n>Subject is required.</span>
+          </div>
+        </div>
+
+        <!-- description -->
+        <div class="form-group row">
+          <label class="cd-col-form-label required"
+                 for="description"
+                 i18n>Description</label>
+          <div class="cd-col-form-input">
+            <textarea id="description"
+                      type="text"
+                      formControlName="description"
+                      class="form-control"
+                      placeholder="Add issue description">
+            </textarea>
+            <span class="invalid-feedback"
+                  *ngIf="feedbackForm.showError('description', formDir, 'required')"
+                  i18n>Description is required.</span>
+          </div>
+        </div>
+
+      </div>
+      <div class="modal-footer">
+        <cd-form-button-panel (submitActionEvent)="onSubmit()"
+                              [form]="feedbackForm"
+                              [submitText]="actionLabels.SUBMIT"
+                              wrappingClass="text-right">
+        </cd-form-button-panel>
+      </div>
+    </form>
+  </div>
+</cd-modal>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/shared/feedback/feedback.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/shared/feedback/feedback.component.scss
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/shared/feedback/feedback.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/shared/feedback/feedback.component.spec.ts
new file mode 100644 (file)
index 0000000..2deea36
--- /dev/null
@@ -0,0 +1,73 @@
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { ReactiveFormsModule } from '@angular/forms';
+import { RouterTestingModule } from '@angular/router/testing';
+
+import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
+import { ToastrModule } from 'ngx-toastr';
+import { throwError } from 'rxjs';
+
+import { FeedbackService } from '~/app/shared/api/feedback.service';
+import { ComponentsModule } from '~/app/shared/components/components.module';
+import { configureTestBed, FormHelper } from '~/testing/unit-test-helper';
+import { FeedbackComponent } from './feedback.component';
+
+describe('FeedbackComponent', () => {
+  let component: FeedbackComponent;
+  let fixture: ComponentFixture<FeedbackComponent>;
+  let feedbackService: FeedbackService;
+  let formHelper: FormHelper;
+
+  configureTestBed({
+    imports: [
+      ComponentsModule,
+      HttpClientTestingModule,
+      RouterTestingModule,
+      ReactiveFormsModule,
+      ToastrModule.forRoot()
+    ],
+    declarations: [FeedbackComponent],
+    providers: [NgbActiveModal]
+  });
+
+  beforeEach(() => {
+    fixture = TestBed.createComponent(FeedbackComponent);
+    component = fixture.componentInstance;
+    feedbackService = TestBed.inject(FeedbackService);
+    fixture.detectChanges();
+  });
+
+  it('should create', () => {
+    expect(component).toBeTruthy();
+  });
+
+  it('should open the form in a modal', () => {
+    const nativeEl = fixture.debugElement.nativeElement;
+    expect(nativeEl.querySelector('cd-modal')).not.toBe(null);
+  });
+
+  it('should redirect to mgr-modules if feedback module is not enabled', () => {
+    spyOn(feedbackService, 'isKeyExist').and.returnValue(throwError({ status: 400 }));
+
+    component.ngOnInit();
+
+    expect(component.isFeedbackEnabled).toEqual(false);
+    expect(component.feedbackForm.disabled).toBeTruthy();
+  });
+
+  it('should test invalid api-key', () => {
+    component.ngOnInit();
+    formHelper = new FormHelper(component.feedbackForm);
+
+    spyOn(feedbackService, 'createIssue').and.returnValue(throwError({ status: 400 }));
+
+    formHelper.setValue('api_key', 'invalidkey');
+    formHelper.setValue('project', 'dashboard');
+    formHelper.setValue('tracker', 'bug');
+    formHelper.setValue('subject', 'foo');
+    formHelper.setValue('description', 'foo');
+    component.onSubmit();
+
+    formHelper.expectError('api_key', 'invalidApiKey');
+  });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/shared/feedback/feedback.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/shared/feedback/feedback.component.ts
new file mode 100644 (file)
index 0000000..91a49a1
--- /dev/null
@@ -0,0 +1,109 @@
+import { Component, OnDestroy, OnInit } from '@angular/core';
+import { FormControl, Validators } from '@angular/forms';
+import { Router } from '@angular/router';
+
+import { NgbActiveModal, NgbModal } from '@ng-bootstrap/ng-bootstrap';
+import { Subscription } from 'rxjs';
+
+import { FeedbackService } from '~/app/shared/api/feedback.service';
+import { ActionLabelsI18n } from '~/app/shared/constants/app.constants';
+import { NotificationType } from '~/app/shared/enum/notification-type.enum';
+import { CdFormGroup } from '~/app/shared/forms/cd-form-group';
+import { NotificationService } from '~/app/shared/services/notification.service';
+
+@Component({
+  selector: 'cd-feedback',
+  templateUrl: './feedback.component.html',
+  styleUrls: ['./feedback.component.scss']
+})
+export class FeedbackComponent implements OnInit, OnDestroy {
+  title = 'Feedback';
+  project: any = [
+    'dashboard',
+    'block',
+    'objects',
+    'file_system',
+    'ceph_manager',
+    'orchestrator',
+    'ceph_volume',
+    'core_ceph'
+  ];
+  tracker: string[] = ['bug', 'feature'];
+  api_key: string;
+  keySub: Subscription;
+
+  feedbackForm: CdFormGroup;
+  isAPIKeySet = false;
+  isFeedbackEnabled = true;
+
+  constructor(
+    private feedbackService: FeedbackService,
+    public activeModal: NgbActiveModal,
+    public actionLabels: ActionLabelsI18n,
+    public secondaryModal: NgbModal,
+    private notificationService: NotificationService,
+    private router: Router
+  ) {}
+
+  ngOnInit() {
+    this.createForm();
+    this.keySub = this.feedbackService.isKeyExist().subscribe({
+      next: (data: boolean) => {
+        this.isAPIKeySet = data;
+        if (this.isAPIKeySet) {
+          this.feedbackForm.get('api_key').clearValidators();
+        }
+      },
+      error: () => {
+        this.isFeedbackEnabled = false;
+        this.feedbackForm.disable();
+      }
+    });
+  }
+
+  private createForm() {
+    this.feedbackForm = new CdFormGroup({
+      project: new FormControl('', Validators.required),
+      tracker: new FormControl('', Validators.required),
+      subject: new FormControl('', Validators.required),
+      description: new FormControl('', Validators.required),
+      api_key: new FormControl('', Validators.required)
+    });
+  }
+
+  ngOnDestroy() {
+    this.keySub.unsubscribe();
+  }
+
+  onSubmit() {
+    this.feedbackService
+      .createIssue(
+        this.feedbackForm.controls['project'].value,
+        this.feedbackForm.controls['tracker'].value,
+        this.feedbackForm.controls['subject'].value,
+        this.feedbackForm.controls['description'].value,
+        this.feedbackForm.controls['api_key'].value
+      )
+      .subscribe({
+        next: (result) => {
+          this.notificationService.show(
+            NotificationType.success,
+            $localize`Issue successfully created on Ceph Issue tracker`,
+            `Go to the tracker: <a href="https://tracker.ceph.com/issues/${result['message']['issue']['id']}" target="_blank"> ${result['message']['issue']['id']} </a>`
+          );
+        },
+        error: () => {
+          this.feedbackForm.get('api_key').setErrors({ invalidApiKey: true });
+          this.feedbackForm.setErrors({ cdSubmitButton: true });
+        },
+        complete: () => {
+          this.activeModal.close();
+        }
+      });
+  }
+
+  redirect() {
+    this.activeModal.close();
+    this.router.navigate(['/mgr-modules']);
+  }
+}
index 274ec71df78b13619396d66bed0e186429008f4c..ed5ad8f1535a7005b8159dbc0684d9fae432f38d 100644 (file)
@@ -21,5 +21,8 @@
     <button ngbDropdownItem
             (click)="openAboutModal()"
             i18n>About</button>
+    <button ngbDropdownItem
+            (click)="openFeedbackModal()"
+            i18n>Report an issue...</button>
   </div>
 </div>
index 910a613335b2ec23f7f9611e04d50c6a9bf95c38..88da15472e3e55a6cb9bb6f5d3feed9822c84dd3 100644 (file)
@@ -2,6 +2,7 @@ import { Component, OnInit } from '@angular/core';
 
 import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap';
 
+import { FeedbackComponent } from '~/app/ceph/shared/feedback/feedback.component';
 import { Icons } from '~/app/shared/enum/icons.enum';
 import { DocService } from '~/app/shared/services/doc.service';
 import { ModalService } from '~/app/shared/services/modal.service';
@@ -16,6 +17,7 @@ export class DashboardHelpComponent implements OnInit {
   docsUrl: string;
   modalRef: NgbModalRef;
   icons = Icons;
+  bsModalRef: NgbModalRef;
 
   constructor(private modalService: ModalService, private docService: DocService) {}
 
@@ -28,4 +30,8 @@ export class DashboardHelpComponent implements OnInit {
   openAboutModal() {
     this.modalRef = this.modalService.show(AboutComponent, null, { size: 'lg' });
   }
+
+  openFeedbackModal() {
+    this.bsModalRef = this.modalService.show(FeedbackComponent, null, { size: 'lg' });
+  }
 }
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/feedback.service.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/feedback.service.spec.ts
new file mode 100644 (file)
index 0000000..ee0becd
--- /dev/null
@@ -0,0 +1,47 @@
+import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
+import { TestBed } from '@angular/core/testing';
+
+import { configureTestBed } from '~/testing/unit-test-helper';
+import { FeedbackService } from './feedback.service';
+
+describe('FeedbackService', () => {
+  let service: FeedbackService;
+  let httpTesting: HttpTestingController;
+
+  configureTestBed({
+    imports: [HttpClientTestingModule],
+    providers: [FeedbackService]
+  });
+
+  beforeEach(() => {
+    service = TestBed.inject(FeedbackService);
+    httpTesting = TestBed.inject(HttpTestingController);
+  });
+
+  afterEach(() => {
+    httpTesting.verify();
+  });
+
+  it('should be created', () => {
+    expect(service).toBeTruthy();
+  });
+
+  it('should call checkAPIKey', () => {
+    service.isKeyExist().subscribe();
+    const req = httpTesting.expectOne('ui-api/feedback/api_key/exist');
+    expect(req.request.method).toBe('GET');
+  });
+
+  it('should call createIssue to create issue tracker', () => {
+    service.createIssue('dashboard', 'bug', 'test', 'test', '').subscribe();
+    const req = httpTesting.expectOne('api/feedback');
+    expect(req.request.method).toBe('POST');
+    expect(req.request.body).toEqual({
+      api_key: '',
+      description: 'test',
+      project: 'dashboard',
+      subject: 'test',
+      tracker: 'bug'
+    });
+  });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/feedback.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/feedback.service.ts
new file mode 100644 (file)
index 0000000..c450bbe
--- /dev/null
@@ -0,0 +1,38 @@
+import { HttpClient } from '@angular/common/http';
+import { Injectable } from '@angular/core';
+
+import * as _ from 'lodash';
+
+@Injectable({
+  providedIn: 'root'
+})
+export class FeedbackService {
+  constructor(private http: HttpClient) {}
+  baseUIURL = 'api/feedback';
+
+  isKeyExist() {
+    return this.http.get('ui-api/feedback/api_key/exist');
+  }
+
+  createIssue(
+    project: string,
+    tracker: string,
+    subject: string,
+    description: string,
+    apiKey: string
+  ) {
+    return this.http.post(
+      'api/feedback',
+      {
+        project: project,
+        tracker: tracker,
+        subject: subject,
+        description: description,
+        api_key: apiKey
+      },
+      {
+        headers: { Accept: 'application/vnd.ceph.api.v0.1+json' }
+      }
+    );
+  }
+}
diff --git a/src/pybind/mgr/dashboard/model/feedback.py b/src/pybind/mgr/dashboard/model/feedback.py
deleted file mode 100644 (file)
index 902f182..0000000
+++ /dev/null
@@ -1,47 +0,0 @@
-# # -*- 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 17e94b25b53372ebe2a3cd19f2396d1db196e0ae..e2cf3487d4c240d86c6c959c5b6873dff3ceff2b 100644 (file)
@@ -19,19 +19,16 @@ if TYPE_CHECKING:
     else:
         from typing_extensions import Literal
 
-from mgr_module import CLICommand, CLIWriteCommand, HandleCommandResult, \
-    MgrModule, MgrStandbyModule, NotifyType, Option, _get_localized_key
+from mgr_module import CLIWriteCommand, HandleCommandResult, MgrModule, \
+    MgrStandbyModule, NotifyType, 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 Router, 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
@@ -411,45 +408,6 @@ 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 46785f57c83762b728579fa5f9b533a05b7d526b..43532585aa8e68ef70f46162947a47b11ee92444 100644 (file)
@@ -2718,17 +2718,41 @@ paths:
       tags:
       - FeatureTogglesEndpoint
   /api/feedback:
+    get:
+      description: "\n        List all issues details.\n        "
+      parameters: []
+      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
     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        "
+        \ of the issue.\n        :param api_key: Ceph tracker api key.\n        "
       parameters: []
       requestBody:
         content:
           application/json:
             schema:
               properties:
+                api_key:
+                  type: string
                 description:
                   type: string
                 project:
@@ -2746,14 +2770,42 @@ paths:
       responses:
         '201':
           content:
-            application/vnd.ceph.api.v1.0+json:
+            application/vnd.ceph.api.v0.1+json:
               type: object
           description: Resource created.
         '202':
           content:
-            application/vnd.ceph.api.v1.0+json:
+            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/api_key:
+    delete:
+      description: "\n        Deletes Ceph tracker API key.\n        "
+      parameters: []
+      responses:
+        '202':
+          content:
+            application/vnd.ceph.api.v0.1+json:
               type: object
           description: Operation is still executing. Please check the task queue.
+        '204':
+          content:
+            application/vnd.ceph.api.v0.1+json:
+              type: object
+          description: Resource deleted.
         '400':
           description: Operation exception. Please check the response body for details.
         '401':
@@ -2767,20 +2819,13 @@ paths:
       - 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
+      description: "\n        Returns Ceph tracker API key.\n        "
+      parameters: []
       responses:
         '200':
           content:
-            application/vnd.ceph.api.v1.0+json:
+            application/vnd.ceph.api.v0.1+json:
               type: object
           description: OK
         '400':
@@ -2796,6 +2841,44 @@ paths:
       - jwt: []
       tags:
       - Report
+    post:
+      description: "\n        Sets Ceph tracker API key.\n        :param api_key:\
+        \ The Ceph tracker API key.\n        "
+      parameters: []
+      requestBody:
+        content:
+          application/json:
+            schema:
+              properties:
+                api_key:
+                  type: string
+              required:
+              - api_key
+              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/grafana/dashboards:
     post:
       parameters: []
@@ -10412,7 +10495,7 @@ tags:
   name: RbdSnapshot
 - description: RBD Trash Management API
   name: RbdTrash
-- description: Feedback API
+- description: Feedback
   name: Report
 - description: RGW Management API
   name: Rgw
diff --git a/src/pybind/mgr/dashboard/services/feedback.py b/src/pybind/mgr/dashboard/services/feedback.py
deleted file mode 100644 (file)
index 7e4c7ad..0000000
+++ /dev/null
@@ -1,85 +0,0 @@
-# -*- 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/feedback/__init__.py b/src/pybind/mgr/feedback/__init__.py
new file mode 100644 (file)
index 0000000..0bc7059
--- /dev/null
@@ -0,0 +1,2 @@
+# flake8: noqa
+from .module import FeedbackModule
\ No newline at end of file
diff --git a/src/pybind/mgr/feedback/model.py b/src/pybind/mgr/feedback/model.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
+            }
+        }
diff --git a/src/pybind/mgr/feedback/module.py b/src/pybind/mgr/feedback/module.py
new file mode 100644 (file)
index 0000000..9568391
--- /dev/null
@@ -0,0 +1,139 @@
+
+"""
+Feedback module
+
+See doc/mgr/feedback.rst for more info.
+"""
+
+from requests.exceptions import RequestException
+
+from mgr_module import CLIReadCommand, HandleCommandResult, MgrModule
+import errno
+
+from .service import CephTrackerClient
+from .model import Feedback
+
+
+class FeedbackModule(MgrModule):
+
+    # there are CLI commands we implement
+    @CLIReadCommand('feedback set api-key')
+    def _cmd_feedback_set_api_key(self, key: str) -> HandleCommandResult:
+        """
+        Set Ceph Issue Tracker API key
+        """
+        try:
+            self.set_store('api_key', key)
+        except Exception as error:
+            return HandleCommandResult(stderr=f'Exception in setting API key : {error}')
+        return HandleCommandResult(stdout="Successfully updated API key")
+
+    @CLIReadCommand('feedback delete api-key')
+    def _cmd_feedback_delete_api_key(self) -> HandleCommandResult:
+        """
+        Delete Ceph Issue Tracker API key
+        """
+        try:
+            self.set_store('api_key', None)
+        except Exception as error:
+            return HandleCommandResult(stderr=f'Exception in deleting API key : {error}')
+        return HandleCommandResult(stdout="Successfully deleted key")
+
+    @CLIReadCommand('feedback get api-key')
+    def _cmd_feedback_get_api_key(self) -> HandleCommandResult:
+        """
+        Get Ceph Issue Tracker API key
+        """
+        try:
+            key = self.get_store('api_key')
+            if key is None:
+                return HandleCommandResult(stderr='Issue tracker key is not set. Set key with `ceph feedback api-key set <your_key>`')
+        except Exception as error:
+            return HandleCommandResult(stderr=f'Error in retreiving issue tracker API key: {error}')
+        return HandleCommandResult(stdout=f'Your key: {key}')
+
+    @CLIReadCommand('feedback issue list')
+    def _cmd_feedback_issue_list(self) -> HandleCommandResult:
+        """
+        Fetch issue list
+        """
+        tracker_client = CephTrackerClient()
+        try:
+            response = tracker_client.list_issues()
+        except Exception:
+            return HandleCommandResult(stderr="Error occurred. Try again later")
+        return HandleCommandResult(stdout=str(response))
+
+    @CLIReadCommand('feedback issue report')
+    def _cmd_feedback_issue_report(self, project: str, tracker: str, subject: str, description: str) -> HandleCommandResult:
+        """
+        Create an issue
+        """
+        try:
+            feedback = Feedback(Feedback.Project[project].value,
+                                Feedback.TrackerType[tracker].value, subject, description)
+        except KeyError:
+            return -errno.EINVAL, '', 'Invalid arguments'
+        try:
+            current_api_key = self.get_store('api_key')
+            if current_api_key is None:
+                return HandleCommandResult(stderr='Issue tracker key is not set. Set key with `ceph set issue_key <your_key>`')
+        except Exception as error:
+            return HandleCommandResult(stderr=f'Error in retreiving issue tracker API key: {error}')
+        tracker_client = CephTrackerClient()
+        try:
+            response = tracker_client.create_issue(feedback, current_api_key)
+        except RequestException as error:
+            return HandleCommandResult(stderr=f'Error in creating issue: {str(error)}. Please set valid API key.')
+        return HandleCommandResult(stdout=f'{str(response)}')
+
+    def set_api_key(self, key: str):
+        try:
+            self.set_store('api_key', key)
+        except Exception as error:
+            raise RequestException(f'Exception in setting API key : {error}')
+        return 'Successfully updated API key'
+
+    def get_api_key(self):
+        try:
+            key = self.get_store('api_key')
+        except Exception as error:
+            raise RequestException(f'Error in retreiving issue tracker API key : {error}')
+        return key
+    
+    def is_api_key_set(self):
+        try:
+            key = self.get_store('api_key')
+        except Exception as error:
+            raise RequestException(f'Error in retreiving issue tracker API key : {error}')
+        if key is None:
+            return False
+        return key != ''
+
+    def delete_api_key(self):
+        try:
+            self.set_store('api_key', None)
+        except Exception as error:
+            raise RequestException(f'Exception in deleting API key : {error}')
+        return 'Successfully deleted API key'
+
+    def get_issues(self):
+        tracker_client = CephTrackerClient()
+        return tracker_client.list_issues()
+
+    def validate_and_create_issue(self, project: str, tracker: str, subject: str, description: str, api_key=None):
+        feedback = Feedback(Feedback.Project[project].value,
+                                Feedback.TrackerType[tracker].value, subject, description)
+        tracker_client = CephTrackerClient()
+        stored_api_key = self.get_store('api_key')
+        try:
+            if api_key:
+                result = tracker_client.create_issue(feedback, api_key)
+            else:
+                result = tracker_client.create_issue(feedback, stored_api_key)
+        except RequestException:
+            self.set_store('api_key', None)
+            raise
+        if not stored_api_key:
+            self.set_store('api_key', api_key)
+        return result
diff --git a/src/pybind/mgr/feedback/service.py b/src/pybind/mgr/feedback/service.py
new file mode 100644 (file)
index 0000000..dc8c6b6
--- /dev/null
@@ -0,0 +1,49 @@
+# -*- coding: utf-8 -*-
+
+import json
+import requests
+from requests.exceptions import RequestException
+
+from .model import Feedback
+
+class config:
+    url = 'tracker.ceph.com'
+    port = 443
+
+class CephTrackerClient():
+
+    def list_issues(self):
+        '''
+        Fetch an issue from the Ceph Issue tracker
+        '''
+        headers = {
+            'Content-Type': 'application/json',
+        }
+        response = requests.get(
+            f'https://{config.url}/issues.json', headers=headers)
+        if not response.ok:
+            if response.status_code == 404:
+                raise FileNotFoundError
+            raise RequestException(response.status_code)
+        return {"message": response.json()}
+
+    def create_issue(self, feedback: Feedback, api_key: str):
+        '''
+        Create an issue in the Ceph Issue tracker
+        '''
+        try:
+            headers = {
+                'Content-Type': 'application/json',
+                'X-Redmine-API-Key': api_key,
+            }
+        except KeyError:
+            raise Exception("Ceph Tracker API Key not set")
+        data = json.dumps(feedback.as_dict())
+        response = requests.post(
+            f'https://{config.url}/projects/{feedback.project_id}/issues.json',
+            headers=headers, data=data)
+        if not response.ok:
+            if response.status_code == 401:
+                raise RequestException("Unauthorized. Invalid issue tracker API key")
+            raise RequestException(response.reason)
+        return {"message": response.json()}