From: Sagar Gopale Date: Fri, 30 Jan 2026 06:42:12 +0000 (+0530) Subject: mgr/dashboard: delete-gateway-nodes X-Git-Tag: testing/wip-vshankar-testing-20260219.071614~3^2 X-Git-Url: http://git-server-git.apps.pok.os.sepia.ceph.com/?a=commitdiff_plain;h=124edb2e30d4e7ff49864f979c96bc2308acc857;p=ceph-ci.git mgr/dashboard: delete-gateway-nodes Fixes: https://tracker.ceph.com/issues/74336 Signed-off-by: Sagar Gopale --- diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-gateway-node/nvmeof-gateway-node.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-gateway-node/nvmeof-gateway-node.component.html index 77fc7e619fc..9da8c7c14ea 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-gateway-node/nvmeof-gateway-node.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-gateway-node/nvmeof-gateway-node.component.html @@ -74,3 +74,5 @@ - } + + diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-gateway-node/nvmeof-gateway-node.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-gateway-node/nvmeof-gateway-node.component.spec.ts index 3603b2c15c1..976c8e39600 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-gateway-node/nvmeof-gateway-node.component.spec.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-gateway-node/nvmeof-gateway-node.component.spec.ts @@ -10,13 +10,19 @@ import { CephModule } from '~/app/ceph/ceph.module'; import { CephSharedModule } from '~/app/ceph/shared/ceph-shared.module'; import { CoreModule } from '~/app/core/core.module'; import { HostService } from '~/app/shared/api/host.service'; +import { CephServiceService } from '~/app/shared/api/ceph-service.service'; import { NvmeofService } from '~/app/shared/api/nvmeof.service'; import { OrchestratorService } from '~/app/shared/api/orchestrator.service'; +import { DeleteConfirmationModalComponent } from '~/app/shared/components/delete-confirmation-modal/delete-confirmation-modal.component'; +import { DeletionImpact } from '~/app/shared/enum/delete-confirmation-modal-impact.enum'; import { CdTableFetchDataContext } from '~/app/shared/models/cd-table-fetch-data-context'; import { CdTableSelection } from '~/app/shared/models/cd-table-selection'; import { HostStatus } from '~/app/shared/enum/host-status.enum'; import { Permissions } from '~/app/shared/models/permissions'; import { AuthStorageService } from '~/app/shared/services/auth-storage.service'; +import { ModalCdsService } from '~/app/shared/services/modal-cds.service'; +import { NotificationService } from '~/app/shared/services/notification.service'; +import { TaskWrapperService } from '~/app/shared/services/task-wrapper.service'; import { SharedModule } from '~/app/shared/shared.module'; import { configureTestBed } from '~/testing/unit-test-helper'; import { TagModule } from 'carbon-components-angular'; @@ -28,6 +34,9 @@ describe('NvmeofGatewayNodeComponent', () => { let hostService: HostService; let orchService: OrchestratorService; let nvmeofService: NvmeofService; + let cephServiceService: CephServiceService; + let modalService: ModalCdsService; + let taskWrapperService: TaskWrapperService; const fakeAuthStorageService = { getPermissions: () => { @@ -113,7 +122,11 @@ describe('NvmeofGatewayNodeComponent', () => { data: { mode: 'selector' } } } - } + }, + ModalCdsService, + CephServiceService, + TaskWrapperService, + NotificationService ] }); @@ -123,6 +136,9 @@ describe('NvmeofGatewayNodeComponent', () => { hostService = TestBed.inject(HostService); orchService = TestBed.inject(OrchestratorService); nvmeofService = TestBed.inject(NvmeofService); + cephServiceService = TestBed.inject(CephServiceService); + modalService = TestBed.inject(ModalCdsService); + taskWrapperService = TestBed.inject(TaskWrapperService); }); it('should create', () => { @@ -417,4 +433,56 @@ describe('NvmeofGatewayNodeComponent', () => { component.ngOnInit(); expect(component.selectionType).toBe('single'); }); + + it('should remove gateway', () => { + spyOn(modalService, 'show').and.callFake((_component, options) => { + options.submitActionObservable(); + return undefined; + }); + const updateSpy = spyOn(cephServiceService, 'update').and.returnValue(of(null)); + component.serviceSpec = { + service_name: 'nvmeof.group1', + placement: { + hosts: ['host1', 'host2'] + } + } as any; + component.selection.selected = [{ hostname: 'host1' }] as any; + + component.removeGateway(); + + expect(modalService.show).toHaveBeenCalledWith(DeleteConfirmationModalComponent, { + itemDescription: 'gateway node', + itemNames: ['host1'], + actionDescription: 'remove', + hideDefaultWarning: true, + impact: DeletionImpact.high, + + bodyContext: { + deletionMessage: + 'Removing host1 will detach it from the gateway group and stop handling new I/O requests. Active connections may be disrupted.

You can re-add this node later if required.' + }, + submitActionObservable: jasmine.any(Function) + }); + + const call = (modalService.show as jasmine.Spy).calls.mostRecent().args[1] + .submitActionObservable; + spyOn(taskWrapperService, 'wrapTaskAroundCall').and.callFake((args: any) => args.call); + call(); + + expect(taskWrapperService.wrapTaskAroundCall).toHaveBeenCalledWith({ + task: jasmine.objectContaining({ + name: 'nvmeof/gateway-node/delete', + metadata: { + hostname: 'host1' + } + }), + call: jasmine.any(Object) + }); + expect(updateSpy).toHaveBeenCalledWith({ + service_name: 'nvmeof.group1', + placement: { + hosts: ['host2'] + } + }); + }); }); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-gateway-node/nvmeof-gateway-node.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-gateway-node/nvmeof-gateway-node.component.ts index da53841521e..833d31aaf62 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-gateway-node/nvmeof-gateway-node.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-gateway-node/nvmeof-gateway-node.component.ts @@ -9,8 +9,10 @@ import { ViewChild } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; -import { Observable, Subject, Subscription } from 'rxjs'; -import { finalize } from 'rxjs/operators'; +import { Observable, Subject, Subscription, of } from 'rxjs'; +import { catchError, finalize, tap } from 'rxjs/operators'; + +import _ from 'lodash'; import { TableComponent } from '~/app/shared/datatable/table/table.component'; import { HostStatus } from '~/app/shared/enum/host-status.enum'; @@ -26,9 +28,16 @@ import { Permission } from '~/app/shared/models/permissions'; import { Host } from '~/app/shared/models/host.interface'; import { CephServiceSpec } from '~/app/shared/models/service.interface'; import { AuthStorageService } from '~/app/shared/services/auth-storage.service'; +import { CephServiceService } from '~/app/shared/api/ceph-service.service'; import { NvmeofService } from '~/app/shared/api/nvmeof.service'; import { ModalCdsService } from '~/app/shared/services/modal-cds.service'; import { NvmeofGatewayNodeAddModalComponent } from './nvmeof-gateway-node-add-modal/nvmeof-gateway-node-add-modal.component'; +import { DeleteConfirmationModalComponent } from '~/app/shared/components/delete-confirmation-modal/delete-confirmation-modal.component'; +import { DeletionImpact } from '~/app/shared/enum/delete-confirmation-modal-impact.enum'; +import { TaskWrapperService } from '~/app/shared/services/task-wrapper.service'; +import { FinishedTask } from '~/app/shared/models/finished-task'; +import { NotificationService } from '~/app/shared/services/notification.service'; +import { NotificationType } from '~/app/shared/enum/notification-type.enum'; @Component({ selector: 'cd-nvmeof-gateway-node', @@ -81,8 +90,11 @@ export class NvmeofGatewayNodeComponent implements OnInit, OnDestroy { constructor( private authStorageService: AuthStorageService, private nvmeofService: NvmeofService, + private cephServiceService: CephServiceService, + private modalService: ModalCdsService, private route: ActivatedRoute, - private modalService: ModalCdsService + private taskWrapper: TaskWrapperService, + private notificationService: NotificationService ) { this.permission = this.authStorageService.getPermissions().nvmeof; } @@ -163,7 +175,45 @@ export class NvmeofGatewayNodeComponent implements OnInit, OnDestroy { } removeGateway(): void { - // TODO + const hostname = this.selection.first().hostname; + this.modalService.show(DeleteConfirmationModalComponent, { + itemDescription: $localize`gateway node`, + itemNames: [hostname], + actionDescription: $localize`remove`, + hideDefaultWarning: true, + impact: DeletionImpact.high, + bodyContext: { + deletionMessage: $localize`Removing ${hostname} will detach it from the gateway group and stop handling new I/O requests. Active connections may be disrupted.

You can re-add this node later if required.` + }, + submitActionObservable: () => { + const updatedSpec = _.cloneDeep(this.serviceSpec); + updatedSpec.placement.hosts = updatedSpec.placement.hosts.filter((h) => h !== hostname); + delete updatedSpec.status; + if (updatedSpec['events']) { + delete updatedSpec['events']; + } + return this.taskWrapper + .wrapTaskAroundCall({ + task: new FinishedTask('nvmeof/gateway-node/delete', { + hostname: hostname + }), + call: this.cephServiceService.update(updatedSpec) + }) + .pipe( + tap(() => { + this.table.refreshBtn(); + }), + catchError((error) => { + this.table.refreshBtn(); + this.notificationService.show( + NotificationType.error, + $localize`Failed to remove gateway node ${hostname}. ${error.message}` + ); + return of(null); + }) + ); + } + }); } ngOnDestroy(): void { diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/delete-confirmation-modal/delete-confirmation-modal.component.html b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/delete-confirmation-modal/delete-confirmation-modal.component.html index dde49ee5151..0682cac60be 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/delete-confirmation-modal/delete-confirmation-modal.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/delete-confirmation-modal/delete-confirmation-modal.component.html @@ -54,7 +54,8 @@ }

- Type {{ itemNames[0] }} to confirm (required) + Type {{ itemNames[0] }} to confirm (required) - Confirm delete + Confirm {{ actionDescription }} } @else {

Observable, @Optional() @Inject('callBackAtionObservable') - public callBackAtionObservable?: () => Observable + public callBackAtionObservable?: () => Observable, + @Optional() @Inject('hideDefaultWarning') public hideDefaultWarning?: boolean ) { super(); this.actionDescription = actionDescription || 'delete'; diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/models/delete-confirmation.model.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/models/delete-confirmation.model.ts index 546ac950fdd..101a4bd049a 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/models/delete-confirmation.model.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/models/delete-confirmation.model.ts @@ -3,4 +3,5 @@ export interface DeleteConfirmationBodyContext { disableForm?: boolean; inputLabel?: string; inputPlaceholder?: string; + deletionMessage?: string; } diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/services/task-message.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/task-message.service.ts index 48cc978d279..32394528a8b 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/services/task-message.service.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/task-message.service.ts @@ -381,6 +381,10 @@ export class TaskMessageService { 'nvmeof/gateway/node/add': this.newTaskMessage(this.commonOperations.add, (metadata) => this.nvmeofGatewayNode(metadata) ), + 'nvmeof/gateway-node/delete': this.newTaskMessage( + this.commonOperations.remove, + (metadata) => $localize`gateway node '${metadata.hostname}'` + ), 'nvmeof/subsystem/create': this.newTaskMessage(this.commonOperations.create, (metadata) => this.nvmeofSubsystem(metadata) ),