From: Kiefer Chang Date: Wed, 11 Dec 2019 03:54:50 +0000 (+0800) Subject: mgr/dashboard: support removing OSD in OSDs page X-Git-Tag: v15.1.1~14^2~6 X-Git-Url: http://git-server-git.apps.pok.os.sepia.ceph.com/?a=commitdiff_plain;h=95296cd7d1da9f0386c057f7b9e76590c98fb3b2;p=ceph.git mgr/dashboard: support removing OSD in OSDs page Add frontend codes and unit tests. Fixes: https://tracker.ceph.com/issues/43062 Signed-off-by: Kiefer Chang --- diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-list/osd-list.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-list/osd-list.component.html index f2f0d2fabb3f..f939dc723175 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-list/osd-list.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-list/osd-list.component.html @@ -55,12 +55,13 @@ -
The {selection.hasSingleSelection, select, 1 {OSD is} 0 {OSDs are}} not safe to destroy! + i18n>The {selection.hasSingleSelection, select, 1 {OSD is} 0 {OSDs are}} not safe to be {{ actionDescription }}! {{ message }}
OSD {{ getSelectedOsdIds() | join }} will be {{ actionDescription }} if you proceed. diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-list/osd-list.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-list/osd-list.component.spec.ts index 7064628e38dd..183d67aad598 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-list/osd-list.component.spec.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-list/osd-list.component.spec.ts @@ -72,7 +72,13 @@ describe('OsdListComponent', () => { */ const mockSafeToDestroy = () => { spyOn(TestBed.get(OsdService), 'safeToDestroy').and.callFake(() => - of({ 'safe-to-destroy': true }) + of({ is_safe_to_destroy: true }) + ); + }; + + const mockSafeToDelete = () => { + spyOn(TestBed.get(OsdService), 'safeToDelete').and.callFake(() => + of({ is_safe_to_delete: true }) ); }; @@ -257,7 +263,8 @@ describe('OsdListComponent', () => { 'Mark Down', 'Mark Lost', 'Purge', - 'Destroy' + 'Destroy', + 'Delete' ], primary: { multiple: 'Scrub', executing: 'Edit', single: 'Edit', no: 'Create' } }, @@ -275,7 +282,7 @@ describe('OsdListComponent', () => { primary: { multiple: 'Scrub', executing: 'Edit', single: 'Edit', no: 'Create' } }, 'create,delete': { - actions: ['Create', 'Mark Lost', 'Purge', 'Destroy'], + actions: ['Create', 'Mark Lost', 'Purge', 'Destroy', 'Delete'], primary: { multiple: 'Create', executing: 'Mark Lost', @@ -298,7 +305,8 @@ describe('OsdListComponent', () => { 'Mark Down', 'Mark Lost', 'Purge', - 'Destroy' + 'Destroy', + 'Delete' ], primary: { multiple: 'Scrub', executing: 'Edit', single: 'Edit', no: 'Edit' } }, @@ -307,7 +315,7 @@ describe('OsdListComponent', () => { primary: { multiple: 'Scrub', executing: 'Edit', single: 'Edit', no: 'Edit' } }, delete: { - actions: ['Mark Lost', 'Purge', 'Destroy'], + actions: ['Mark Lost', 'Purge', 'Destroy', 'Delete'], primary: { multiple: 'Mark Lost', executing: 'Mark Lost', @@ -387,13 +395,22 @@ describe('OsdListComponent', () => { expectOpensModal('Mark Lost', modalClass); expectOpensModal('Purge', modalClass); expectOpensModal('Destroy', modalClass); + mockSafeToDelete(); + expectOpensModal('Delete', modalClass); }); }); describe('tests if the correct methods are called on confirmation', () => { const expectOsdServiceMethodCalled = ( actionName: string, - osdServiceMethodName: 'markOut' | 'markIn' | 'markDown' | 'markLost' | 'purge' | 'destroy' + osdServiceMethodName: + | 'markOut' + | 'markIn' + | 'markDown' + | 'markLost' + | 'purge' + | 'destroy' + | 'delete' ): void => { const osdServiceSpy = spyOn(osdService, osdServiceMethodName).and.callFake(() => EMPTY); openActionModal(actionName); @@ -420,6 +437,8 @@ describe('OsdListComponent', () => { expectOsdServiceMethodCalled('Mark Lost', 'markLost'); expectOsdServiceMethodCalled('Purge', 'purge'); expectOsdServiceMethodCalled('Destroy', 'destroy'); + mockSafeToDelete(); + expectOsdServiceMethodCalled('Delete', 'delete'); }); }); }); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-list/osd-list.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-list/osd-list.component.ts index 802d6a5e60b4..79c6b8aea976 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-list/osd-list.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/osd/osd-list/osd-list.component.ts @@ -148,6 +148,10 @@ export class OsdListComponent implements OnInit { this.i18n('Mark'), this.i18n('OSD lost'), this.i18n('marked lost'), + (ids: number[]) => { + return this.osdService.safeToDestroy(JSON.stringify(ids)); + }, + 'is_safe_to_destroy', this.osdService.markLost ), disable: () => this.isNotSelectedOrInState('up'), @@ -161,7 +165,11 @@ export class OsdListComponent implements OnInit { this.i18n('Purge'), this.i18n('OSD'), this.i18n('purged'), - (id) => { + (ids: number[]) => { + return this.osdService.safeToDestroy(JSON.stringify(ids)); + }, + 'is_safe_to_destroy', + (id: number) => { this.selection = new CdTableSelection(); return this.osdService.purge(id); } @@ -177,12 +185,36 @@ export class OsdListComponent implements OnInit { this.i18n('destroy'), this.i18n('OSD'), this.i18n('destroyed'), - (id) => { + (ids: number[]) => { + return this.osdService.safeToDestroy(JSON.stringify(ids)); + }, + 'is_safe_to_destroy', + (id: number) => { this.selection = new CdTableSelection(); return this.osdService.destroy(id); } ), disable: () => this.isNotSelectedOrInState('up'), + icon: Icons.destroyCircle + }, + { + name: this.actionLabels.DELETE, + permission: 'delete', + click: () => + this.showCriticalConfirmationModal( + this.i18n('delete'), + this.i18n('OSD'), + this.i18n('deleted'), + (ids: number[]) => { + return this.osdService.safeToDelete(JSON.stringify(ids)); + }, + 'is_safe_to_delete', + (id: number) => { + this.selection = new CdTableSelection(); + return this.osdService.delete(id, true); + } + ), + disable: () => !this.hasOsdSelected, icon: Icons.destroy } ]; @@ -412,20 +444,34 @@ export class OsdListComponent implements OnInit { }); } + /** + * Perform check first and display a critical confirmation modal. + * @param {string} actionDescription name of the action. + * @param {string} itemDescription the item's name that the action operates on. + * @param {string} templateItemDescription the action name to be displayed in modal template. + * @param {Function} check the function is called to check if the action is safe. + * @param {string} checkKey the safe indicator's key in the check response. + * @param {Function} action the action function. + * @param {boolean} oneshot if true, action function is called with all items as parameter. + * Otherwise, multiple action functions with individual items are sent. + */ showCriticalConfirmationModal( actionDescription: string, itemDescription: string, templateItemDescription: string, - action: (id: number) => Observable + check: (ids: number[]) => Observable, + checkKey: string, + action: (id: number | number[]) => Observable ): void { - this.osdService.safeToDestroy(JSON.stringify(this.getSelectedOsdIds())).subscribe((result) => { + check(this.getSelectedOsdIds()).subscribe((result) => { const modalRef = this.modalService.show(CriticalConfirmationModalComponent, { initialState: { actionDescription: actionDescription, itemDescription: itemDescription, bodyTemplate: this.criticalConfirmationTpl, bodyContext: { - result: result, + safeToPerform: result[checkKey], + message: result.message, actionDescription: templateItemDescription }, submitAction: () => { diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/osd.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/osd.service.ts index 02d99fa1f306..30cdcd1ea369 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/osd.service.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/osd.service.ts @@ -1,4 +1,4 @@ -import { HttpClient } from '@angular/common/http'; +import { HttpClient, HttpParams } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { I18n } from '@ngx-translate/i18n-polyfill'; @@ -138,14 +138,28 @@ export class OsdService { return this.http.post(`${this.path}/${id}/destroy`, null); } + delete(id: number, force?: boolean) { + const options = force ? { params: new HttpParams().set('force', 'true') } : {}; + options['observe'] = 'response'; + return this.http.delete(`${this.path}/${id}`, options); + } + safeToDestroy(ids: string) { interface SafeToDestroyResponse { - 'safe-to-destroy': boolean; + is_safe_to_destroy: boolean; message?: string; } return this.http.get(`${this.path}/safe_to_destroy?ids=${ids}`); } + safeToDelete(ids: string) { + interface SafeToDeleteResponse { + is_safe_to_delete: boolean; + message?: string; + } + return this.http.get(`${this.path}/safe_to_delete?svc_ids=${ids}`); + } + getDevices(osdId: number) { return this.http .get(`${this.path}/${osdId}/devices`)