From 124edb2e30d4e7ff49864f979c96bc2308acc857 Mon Sep 17 00:00:00 2001
From: Sagar Gopale
Date: Fri, 30 Jan 2026 12:12:12 +0530
Subject: [PATCH] mgr/dashboard: delete-gateway-nodes
Fixes: https://tracker.ceph.com/issues/74336
Signed-off-by: Sagar Gopale
---
.../nvmeof-gateway-node.component.html | 2 +
.../nvmeof-gateway-node.component.spec.ts | 70 ++++++++++++++++++-
.../nvmeof-gateway-node.component.ts | 58 +++++++++++++--
.../delete-confirmation-modal.component.html | 5 +-
.../delete-confirmation-modal.component.ts | 3 +-
.../models/delete-confirmation.model.ts | 1 +
.../shared/services/task-message.service.ts | 4 ++
7 files changed, 135 insertions(+), 8 deletions(-)
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)
),
--
2.47.3