From: Afreen Misbah Date: Mon, 9 Feb 2026 16:15:03 +0000 (+0530) Subject: mgr/dashboard: Add apis for add/del hosts on namespaces X-Git-Url: http://git-server-git.apps.pok.os.sepia.ceph.com/?a=commitdiff_plain;h=a67db3275744196ded4c8d8b67dfbfeaa5494cda;p=ceph.git mgr/dashboard: Add apis for add/del hosts on namespaces - these are UI APIs - also removed namespace API and using "*" in existing instead for getting all ns in subsystem Signed-off-by: Afreen Misbah --- diff --git a/src/pybind/mgr/dashboard/controllers/nvmeof.py b/src/pybind/mgr/dashboard/controllers/nvmeof.py index f6f022d1d14..63a4d7fe720 100644 --- a/src/pybind/mgr/dashboard/controllers/nvmeof.py +++ b/src/pybind/mgr/dashboard/controllers/nvmeof.py @@ -474,27 +474,6 @@ else: ) ) - @APIRouter("/nvmeof/gateway_group/{gw_group}/namespace", Scope.NVME_OF) - @APIDoc("NVMe-oF Gateway Management API", "NVMe-oF Gateway") - class NVMeoFGatewayGroup(RESTController): - @Endpoint('GET') - @EndpointDoc( - "List all NVMeoF namespaces in a gateway group", - parameters={ - "gw_group": Param(str, "NVMeoF gateway group", True, None), - "server_address": Param(str, "NVMeoF gateway address", True, None), - }, - ) - @convert_to_model(model.NamespaceList) - @handle_nvmeof_error - def list(self, gw_group: str, server_address: Optional[str] = None): - return NVMeoFClient( - gw_group=gw_group, - server_address=server_address - ).stub.list_namespaces( - NVMeoFClient.pb2.list_namespaces_req() - ) - @APIRouter("/nvmeof/subsystem/{nqn}/namespace", Scope.NVME_OF) @APIDoc("NVMe-oF Subsystem Namespace Management API", "NVMe-oF Subsystem Namespace") class NVMeoFNamespace(RESTController): @@ -1423,6 +1402,7 @@ else: status["available"] = False status["message"] = 'An NVMe/TCP service must be created.' return status + # UI API for adding one or more than one hosts to subsystem @Endpoint('POST', "/subsystem/{subsystem_nqn}/host") @EndpointDoc("Add one or more initiator hosts to an NVMeoF subsystem", @@ -1434,41 +1414,22 @@ else: @empty_response @handle_nvmeof_error @CreatePermission - def add(self, subsystem_nqn: str, host_nqn: str, dhchap_key: Optional[str] = None, - psk: Optional[str] = None, gw_group: Optional[str] = None, - server_address: Optional[str] = None - ): + def add(self, subsystem_nqn: str, + gw_group: str, + host_nqn: str = "", + server_address: Optional[str] = None): response = None + all_host_nqns = host_nqn.split(',') - if host_nqn != '*': - all_host_nqns = host_nqn.split(',') - for nqn in all_host_nqns: - response = NVMeoFClient( - gw_group=gw_group, - server_address=server_address - ).stub.add_host( - NVMeoFClient.pb2.add_host_req( - subsystem_nqn=subsystem_nqn, - host_nqn=nqn, - dhchap_key=dhchap_key, - psk=psk) - ) - if response.status != 0: - return response - else: - response = NVMeoFClient( - gw_group=gw_group, - server_address=server_address - ).stub.add_host( - NVMeoFClient.pb2.add_host_req( - subsystem_nqn=subsystem_nqn, - host_nqn=host_nqn, - dhchap_key=dhchap_key, - psk=psk) + for nqn in all_host_nqns: + response = NVMeoFClient(gw_group=gw_group, + server_address=server_address).stub.add_host( + NVMeoFClient.pb2.add_host_req(subsystem_nqn=subsystem_nqn, host_nqn=nqn) ) if response.status != 0: return response return response + # UI API for deleting one or more than one hosts to subsystem @Endpoint(method='DELETE', path="/subsystem/{subsystem_nqn}/host/{host_nqn}") @EndpointDoc("Remove on or more initiator hosts from an NVMeoF subsystem", @@ -1480,12 +1441,16 @@ else: @empty_response @handle_nvmeof_error @DeletePermission - def remove(self, subsystem_nqn: str, host_nqn: str, gw_group: str): + def remove(self, subsystem_nqn: str, + host_nqn: str, + gw_group: str, + server_address: Optional[str] = None): response = None to_delete_nqns = host_nqn.split(',') for del_nqn in to_delete_nqns: - response = NVMeoFClient(gw_group=gw_group).stub.remove_host( + response = NVMeoFClient(gw_group=gw_group, + server_address=server_address).stub.remove_host( NVMeoFClient.pb2.remove_host_req(subsystem_nqn=subsystem_nqn, host_nqn=del_nqn) ) if response.status != 0: @@ -1493,3 +1458,80 @@ else: logger.info("removed host %s from subsystem %s", del_nqn, subsystem_nqn) return response + # UI API for adding one or more than one hosts to namespace + + @Endpoint('POST', "/namespace/{nsid}/host") + @EndpointDoc("Add one or more initiator hosts to an NVMeoF subsystem", + parameters={ + "subsystem_nqn": Param(str, "NVMeoF subsystem NQN"), + "nsid": Param(str, "NVMeoF Namespace ID"), + "host_nqn": Param(str, 'Comma separated list of NVMeoF host NQN.'), + "force": Param( + bool, + "Allow adding the host to the namespace even if the host " + "has no access to the subsystem" + ), + "gw_group": Param(str, "NVMeoF gateway group", True, None), + "server_address": Param(str, "NVMeoF gateway address", True, None), + }) + @empty_response + @handle_nvmeof_error + @CreatePermission + def add_namesapce_initiator(self, nsid: str, + subsystem_nqn: str, + gw_group: str, + host_nqn: str = "", + force: Optional[bool] = None, + server_address: Optional[str] = None): + response = None + all_host_nqns = host_nqn.split(',') + force = str_to_bool(force) if force else None + + for nqn in all_host_nqns: + response = NVMeoFClient(gw_group=gw_group, + server_address=server_address).stub.namespace_add_host( + NVMeoFClient.pb2.namespace_add_host_req(subsystem_nqn=subsystem_nqn, + nsid=int(nsid), + host_nqn=nqn, + force=force) + ) + if response.status != 0: + return response + return response + # UI API for deleting one or more than one hosts to namespace + + @Endpoint(method='DELETE', path="/namespace/{nsid}/host") + @EndpointDoc("Remove on or more initiator hosts from an NVMeoF subsystem", + parameters={ + "subsystem_nqn": Param(str, "NVMeoF subsystem NQN"), + "nsid": Param(str, "NVMeoF Namespace ID"), + "host_nqn": Param(str, 'Comma separated list of NVMeoF host NQN.'), + "gw_group": Param(str, "NVMeoF gateway group", True, None), + "server_address": Param(str, "NVMeoF gateway address", True, None), + }) + @empty_response + @handle_nvmeof_error + @DeletePermission + def remove_namespace_initiator(self, + nsid: str, + subsystem_nqn: str, + host_nqn: str, + gw_group: str, + server_address: Optional[str] = None): + response = None + to_delete_nqns = host_nqn.split(',') + + for del_nqn in to_delete_nqns: + response = NVMeoFClient(gw_group=gw_group, + server_address=server_address).stub.namespace_delete_host( + NVMeoFClient.pb2.namespace_delete_host_req( + subsystem_nqn=subsystem_nqn, + nsid=int(nsid), + host_nqn=del_nqn, + ) + ) + if response.status != 0: + return response + logger.info("removed host %s from namespace %s", del_nqn, nsid) + + return response diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-initiators-form/nvmeof-initiators-form.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-initiators-form/nvmeof-initiators-form.component.spec.ts index f6da04f5ec0..7df77e1b99d 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-initiators-form/nvmeof-initiators-form.component.spec.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-initiators-form/nvmeof-initiators-form.component.spec.ts @@ -46,14 +46,14 @@ describe('NvmeofInitiatorsFormComponent', () => { describe('should test form', () => { beforeEach(() => { nvmeofService = TestBed.inject(NvmeofService); - spyOn(nvmeofService, 'addInitiators').and.stub(); + spyOn(nvmeofService, 'addSubsystemInitiators').and.stub(); }); it('should be creating request correctly', () => { const subsystemNQN = 'nqn.2001-07.com.ceph:' + mockTimestamp; component.subsystemNQN = subsystemNQN; component.onSubmit(); - expect(nvmeofService.addInitiators).toHaveBeenCalledWith(subsystemNQN, { + expect(nvmeofService.addSubsystemInitiators).toHaveBeenCalledWith(subsystemNQN, { host_nqn: '' }); }); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-initiators-form/nvmeof-initiators-form.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-initiators-form/nvmeof-initiators-form.component.ts index 2c4adf51ae5..4446a2de9cb 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-initiators-form/nvmeof-initiators-form.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-initiators-form/nvmeof-initiators-form.component.ts @@ -127,7 +127,7 @@ export class NvmeofInitiatorsFormComponent implements OnInit { task: new FinishedTask(taskUrl, { nqn: this.subsystemNQN }), - call: this.nvmeofService.addInitiators(this.subsystemNQN, request) + call: this.nvmeofService.addSubsystemInitiators(this.subsystemNQN, request) }) .subscribe({ error() { diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-initiators-list/nvmeof-initiators-list.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-initiators-list/nvmeof-initiators-list.component.ts index 9e14697ab2e..4cccb4e154f 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-initiators-list/nvmeof-initiators-list.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-initiators-list/nvmeof-initiators-list.component.ts @@ -117,7 +117,7 @@ export class NvmeofInitiatorsListComponent implements OnInit { nqn: this.subsystemNQN, plural: itemNames.length > 1 }), - call: this.nvmeofService.removeInitiators(this.subsystemNQN, { + call: this.nvmeofService.removeSubsystemInitiators(this.subsystemNQN, { host_nqn, gw_group: this.group }) diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-subsystems-form/nvmeof-subsystems-form.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-subsystems-form/nvmeof-subsystems-form.component.spec.ts index 4c7e3d6c3f0..78aae18c0f9 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-subsystems-form/nvmeof-subsystems-form.component.spec.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-subsystems-form/nvmeof-subsystems-form.component.spec.ts @@ -73,7 +73,7 @@ describe('NvmeofSubsystemsFormComponent', () => { beforeEach(() => { nvmeofService = TestBed.inject(NvmeofService); spyOn(nvmeofService, 'createSubsystem').and.returnValue(of({})); - spyOn(nvmeofService, 'addInitiators').and.returnValue(of({})); + spyOn(nvmeofService, 'addSubsystemInitiators').and.returnValue(of({})); }); it('should be creating request correctly', () => { @@ -100,7 +100,7 @@ describe('NvmeofSubsystemsFormComponent', () => { component.group = mockGroupName; component.onSubmit(payload); - expect(nvmeofService.addInitiators).toHaveBeenCalledWith('test-nqn.default', { + expect(nvmeofService.addSubsystemInitiators).toHaveBeenCalledWith('test-nqn.default', { host_nqn: '*', gw_group: mockGroupName }); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-subsystems-form/nvmeof-subsystems-form.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-subsystems-form/nvmeof-subsystems-form.component.ts index fb25ba1f815..7de6ae958a5 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-subsystems-form/nvmeof-subsystems-form.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-subsystems-form/nvmeof-subsystems-form.component.ts @@ -97,7 +97,10 @@ export class NvmeofSubsystemsFormComponent implements OnInit { { step: this.steps[1].label, call: () => - this.nvmeofService.addInitiators(`${payload.nqn}.${this.group}`, initiatorRequest) + this.nvmeofService.addSubsystemInitiators( + `${payload.nqn}.${this.group}`, + initiatorRequest + ) } ], stepResults diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/nvmeof.service.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/nvmeof.service.spec.ts index 36c6b752a8b..5873fc79caf 100755 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/nvmeof.service.spec.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/nvmeof.service.spec.ts @@ -199,12 +199,12 @@ describe('NvmeofService', () => { expect(req.request.method).toBe('GET'); }); it('should call addInitiators', () => { - service.addInitiators(mockNQN, request).subscribe(); + service.addSubsystemInitiators(mockNQN, request).subscribe(); const req = httpTesting.expectOne(`${UI_API_PATH}/subsystem/${mockNQN}/host`); expect(req.request.method).toBe('POST'); }); it('should call removeInitiators', () => { - service.removeInitiators(mockNQN, request).subscribe(); + service.removeSubsystemInitiators(mockNQN, request).subscribe(); const req = httpTesting.expectOne( `${UI_API_PATH}/subsystem/${mockNQN}/host/${request.host_nqn}/${mockGroupName}` ); @@ -247,7 +247,9 @@ describe('NvmeofService', () => { const mockNsid = '1'; it('should call listNamespaces', () => { service.listNamespaces(mockGroupName).subscribe(); - const req = httpTesting.expectOne(`${API_PATH}/gateway_group/${mockGroupName}/namespace`); + const req = httpTesting.expectOne( + `${API_PATH}/subsystem/*/namespace?gw_group=${mockGroupName}` + ); expect(req.request.method).toBe('GET'); }); it('should call getNamespace', () => { diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/nvmeof.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/nvmeof.service.ts index 8937eb5026b..3ede27f1ecb 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/nvmeof.service.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/nvmeof.service.ts @@ -35,6 +35,7 @@ export type NamespaceCreateRequest = NvmeofRequest & { rbd_image_name: string; rbd_pool: string; rbd_image_size?: number; + no_auto_visible?: boolean; create_image: boolean; }; @@ -46,6 +47,10 @@ export type InitiatorRequest = NvmeofRequest & { host_nqn: string; }; +export type NamespaceInitiatorRequest = InitiatorRequest & { + subsystem_nqn: string; +}; + const API_PATH = 'api/nvmeof'; const UI_API_PATH = 'ui-api/nvmeof'; @@ -174,13 +179,19 @@ export class NvmeofService { return this.http.get(`${API_PATH}/subsystem/${subsystemNQN}/host?gw_group=${group}`); } - addInitiators(subsystemNQN: string, request: InitiatorRequest) { + addSubsystemInitiators(subsystemNQN: string, request: InitiatorRequest) { return this.http.post(`${UI_API_PATH}/subsystem/${subsystemNQN}/host`, request, { observe: 'response' }); } - removeInitiators(subsystemNQN: string, request: InitiatorRequest) { + addNamespaceInitiators(nsid: string, request: NamespaceInitiatorRequest) { + return this.http.post(`${UI_API_PATH}/namespace/${nsid}/host`, request, { + observe: 'response' + }); + } + + removeSubsystemInitiators(subsystemNQN: string, request: InitiatorRequest) { return this.http.delete( `${UI_API_PATH}/subsystem/${subsystemNQN}/host/${request.host_nqn}/${request.gw_group}`, { @@ -189,6 +200,15 @@ export class NvmeofService { ); } + removeNamespaceInitiators(nsid: string, request: NamespaceInitiatorRequest) { + return this.http.delete( + `${UI_API_PATH}/namespace/${nsid}/host/${request.subsystem_nqn}/${request.host_nqn}/${request.gw_group}`, + { + observe: 'response' + } + ); + } + // Listeners listListeners(subsystemNQN: string, group: string) { return this.http.get(`${API_PATH}/subsystem/${subsystemNQN}/listener?gw_group=${group}`); @@ -221,8 +241,8 @@ export class NvmeofService { } // Namespaces - listNamespaces(group: string) { - return this.http.get(`${API_PATH}/gateway_group/${group}/namespace`); + listNamespaces(group: string, subsystemNQN: string = '*') { + return this.http.get(`${API_PATH}/subsystem/${subsystemNQN}/namespace?gw_group=${group}`); } getNamespace(subsystemNQN: string, nsid: string, group: string) { diff --git a/src/pybind/mgr/dashboard/openapi.yaml b/src/pybind/mgr/dashboard/openapi.yaml index c20e63e4a6a..fd80d9a1466 100755 --- a/src/pybind/mgr/dashboard/openapi.yaml +++ b/src/pybind/mgr/dashboard/openapi.yaml @@ -12696,84 +12696,6 @@ paths: summary: Get the version of the NVMeoF gateway tags: - NVMe-oF Gateway - /api/nvmeof/gateway_group/{gw_group}/namespace: - get: - parameters: - - description: NVMeoF gateway group - in: path - name: gw_group - required: true - schema: - type: string - - allowEmptyValue: true - description: NVMeoF gateway address - in: query - name: server_address - schema: - type: string - responses: - '200': - content: - application/json: - schema: - type: object - application/vnd.ceph.api.v1.0+json: - schema: - 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: [] - summary: List all NVMeoF namespaces in a gateway group - tags: - - NVMe-oF Gateway - /api/nvmeof/gateway_group/{gw_group}/namespace/list: - get: - parameters: - - description: NVMeoF gateway group - in: path - name: gw_group - required: true - schema: - type: string - - allowEmptyValue: true - description: NVMeoF gateway address - in: query - name: server_address - schema: - type: string - responses: - '200': - content: - application/json: - schema: - type: object - application/vnd.ceph.api.v1.0+json: - schema: - 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: [] - summary: List all NVMeoF namespaces in a gateway group - tags: - - NVMe-oF Gateway /api/nvmeof/spdk/log_level: get: parameters: