]> git-server-git.apps.pok.os.sepia.ceph.com Git - ceph-ci.git/commitdiff
mgr/dashboard: delete-gateway-nodes
authorSagar Gopale <sagar.gopale@ibm.com>
Fri, 30 Jan 2026 06:42:12 +0000 (12:12 +0530)
committerSagar Gopale <sagar.gopale@ibm.com>
Tue, 17 Feb 2026 12:47:58 +0000 (18:17 +0530)
Fixes: https://tracker.ceph.com/issues/74336
Signed-off-by: Sagar Gopale <sagar.gopale@ibm.com>
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-gateway-node/nvmeof-gateway-node.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-gateway-node/nvmeof-gateway-node.component.spec.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-gateway-node/nvmeof-gateway-node.component.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/components/delete-confirmation-modal/delete-confirmation-modal.component.html
src/pybind/mgr/dashboard/frontend/src/app/shared/components/delete-confirmation-modal/delete-confirmation-modal.component.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/models/delete-confirmation.model.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/services/task-message.service.ts

index 3603b2c15c192ac08de87599beecf4cfba4e762c..976c8e3960082683050a5deb8766f6d1326bdb8a 100644 (file)
@@ -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 <strong>host1</strong> will detach it from the gateway group and stop handling new I/O requests. Active connections may be disrupted.<br><br>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']
+      }
+    });
+  });
 });
index da53841521efa92cdeb6bb6d61f6e67f257e12af..833d31aaf624b53473d5919324c0882847cc3484 100644 (file)
@@ -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 <strong>${hostname}</strong> will detach it from the gateway group and stop handling new I/O requests. Active connections may be disrupted.<br><br>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 {
index dde49ee5151ccddafa3d4f6f3ba23eedd4a429f5..0682cac60bed073c83011306a073847825643102 100644 (file)
@@ -54,7 +54,8 @@
             }
             </p>
             <ng-template #labelTemplate>
-              <span i18n>Type <strong>{{ itemNames[0] }}</strong> to confirm (required)</span>
+              <span i18n
+                    class="cds--type-label-01">Type <strong>{{ itemNames[0] }}</strong> to confirm (required)</span>
             </ng-template>
             <cds-text-label [labelTemplate]="labelTemplate"
                             labelInputID="resource_name"
   <h2 class="cds--type-heading-03"
       cdsModalHeaderHeading
       i18n>
-    Confirm delete
+    Confirm {{ actionDescription }}
   </h2>
   } @else {
   <h3 cdsModalHeaderHeading
index 66d0ad6c3a3ee477347ef5f5b905b439a4042220..4b34d02a10ff490f0d4df66bed5347fed2024761 100644 (file)
@@ -42,7 +42,8 @@ export class DeleteConfirmationModalComponent extends BaseModal implements OnIni
     public submitActionObservable?: () => Observable<any>,
     @Optional()
     @Inject('callBackAtionObservable')
-    public callBackAtionObservable?: () => Observable<any>
+    public callBackAtionObservable?: () => Observable<any>,
+    @Optional() @Inject('hideDefaultWarning') public hideDefaultWarning?: boolean
   ) {
     super();
     this.actionDescription = actionDescription || 'delete';
index 546ac950fdd55edaad1a8a62aece2fdfed85fece..101a4bd049a6c0da039213696a6e96a2e8ae89f0 100644 (file)
@@ -3,4 +3,5 @@ export interface DeleteConfirmationBodyContext {
   disableForm?: boolean;
   inputLabel?: string;
   inputPlaceholder?: string;
+  deletionMessage?: string;
 }
index 48cc978d279b39f0c3cd43f8a80bfa9a53aeb0b1..32394528a8bbc8cad32ad6edaf9acb15ba942aaf 100644 (file)
@@ -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)
     ),