--- /dev/null
+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)
"progress",
"balancer",
"devicehealth",
+ "feedback",
"orchestrator",
"rbd_support",
"volumes",
# # -*- 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
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';
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';
SharedModule,
ChartsModule,
RouterModule,
- NgbPopoverModule
+ NgbPopoverModule,
+ FormsModule,
+ ReactiveFormsModule
],
declarations: [
MdsSummaryPipe,
HealthPieComponent,
InfoCardComponent,
- InfoGroupComponent
+ InfoGroupComponent,
+ FeedbackComponent
]
})
export class DashboardModule {}
--- /dev/null
+<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>
--- /dev/null
+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');
+ });
+});
--- /dev/null
+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']);
+ }
+}
<button ngbDropdownItem
(click)="openAboutModal()"
i18n>About</button>
+ <button ngbDropdownItem
+ (click)="openFeedbackModal()"
+ i18n>Report an issue...</button>
</div>
</div>
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';
docsUrl: string;
modalRef: NgbModalRef;
icons = Icons;
+ bsModalRef: NgbModalRef;
constructor(private modalService: ModalService, private docService: DocService) {}
openAboutModal() {
this.modalRef = this.modalService.show(AboutComponent, null, { size: 'lg' });
}
+
+ openFeedbackModal() {
+ this.bsModalRef = this.modalService.show(FeedbackComponent, null, { size: 'lg' });
+ }
}
--- /dev/null
+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'
+ });
+ });
+});
--- /dev/null
+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' }
+ }
+ );
+ }
+}
+++ /dev/null
-# # -*- 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
- }
- }
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
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:
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:
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':
- 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':
- 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: []
name: RbdSnapshot
- description: RBD Trash Management API
name: RbdTrash
-- description: Feedback API
+- description: Feedback
name: Report
- description: RGW Management API
name: Rgw
+++ /dev/null
-# -*- 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}
--- /dev/null
+# flake8: noqa
+from .module import FeedbackModule
\ No newline at end of file
--- /dev/null
+# # -*- 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
+ }
+ }
--- /dev/null
+
+"""
+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
--- /dev/null
+# -*- 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()}