From: Tomer Haskalovitch Date: Tue, 2 Jun 2026 10:20:28 +0000 (+0300) Subject: mgr/dashboard: align nvmeof cli with missing parameters and functions from the old... X-Git-Url: http://git-server-git.apps.pok.os.sepia.ceph.com/?a=commitdiff_plain;h=e3d9ebb50e914bfa8a2518a7f22024b0d10f375b;p=ceph.git mgr/dashboard: align nvmeof cli with missing parameters and functions from the old nvmeof cli fixes: https://tracker.ceph.com/issues/77108 Signed-off-by: Tomer Haskalovitch --- diff --git a/src/pybind/mgr/dashboard/controllers/nvmeof.py b/src/pybind/mgr/dashboard/controllers/nvmeof.py index d6f7628803c..557ea546ef1 100644 --- a/src/pybind/mgr/dashboard/controllers/nvmeof.py +++ b/src/pybind/mgr/dashboard/controllers/nvmeof.py @@ -2,7 +2,7 @@ # pylint: disable=too-many-lines import logging from functools import partial -from typing import Any, Dict, List, Optional +from typing import Any, Dict, List, Optional, Tuple import cherrypy from orchestrator import OrchestratorError @@ -299,6 +299,37 @@ else: ) return io_stats + @ReadPermission + @Endpoint('GET', '/thread_stats') + @NvmeofCLICommand( + "nvmeof gateway get_thread_stats", model.ThreadStatsInfo, + alias="nvmeof gw get_thread_stats") + @EndpointDoc( + "Get NVMeoF thread statistics for the gateway", + parameters={ + "gw_group": Param(str, "NVMeoF gateway group", True, None), + "server_address": Param(str, "NVMeoF gateway address", True, None), + "traddr": Param(str, "NVMeoF gateway address (deprecated)", True, None), + } + ) + @convert_to_model(model.ThreadStatsInfo) + @handle_nvmeof_error + def get_thread_stats( + self, gw_group: Optional[str] = None, + server_address: Optional[str] = None, + traddr: Optional[str] = None + ): + server_address = resolve_nvmeof_server_address( + server_address=server_address, + traddr=traddr + ) + return NVMeoFClient( + gw_group=gw_group, + server_address=server_address + ).stub.get_thread_stats( + NVMeoFClient.pb2.get_thread_stats_req() + ) + @APIRouter("/nvmeof/spdk", Scope.NVME_OF) @APIDoc("NVMe-oF SPDK Management API", "NVMe-oF SPDK") class NVMeoFSpdk(RESTController): @@ -416,6 +447,8 @@ else: @EndpointDoc( "List all NVMeoF subsystems", parameters={ + "nqn": Param(str, "NVMeoF subsystem NQN to filter by", True, None), + "serial_number": Param(str, "Serial number to filter by", True, None), "gw_group": Param(str, "NVMeoF gateway group", True, None), "server_address": Param(str, "NVMeoF gateway address", True, None), "traddr": Param(str, "NVMeoF gateway address (deprecated)", True, None), @@ -423,7 +456,8 @@ else: ) @convert_to_model(model.SubsystemList) @handle_nvmeof_error - def list(self, gw_group: Optional[str] = None, server_address: Optional[str] = None, + def list(self, nqn: Optional[str] = None, serial_number: Optional[str] = None, + gw_group: Optional[str] = None, server_address: Optional[str] = None, traddr: Optional[str] = None): server_address = resolve_nvmeof_server_address( server_address=server_address, @@ -433,7 +467,8 @@ else: gw_group=gw_group, server_address=server_address ).stub.list_subsystems( - NVMeoFClient.pb2.list_subsystems_req() + NVMeoFClient.pb2.list_subsystems_req(subsystem_nqn=nqn, + serial_number=serial_number) ) @pick(field="subsystems", first=True) @@ -860,6 +895,12 @@ else: "gw_group": Param(str, "NVMeoF gateway group", True, None), "server_address": Param(str, "NVMeoF gateway address", True, None), "secure": Param(bool, "Use a secure channel", True, False), + "force": Param( + bool, + "Allow contradiction in security with existing listeners", + True, + False + ), "verify_host_name": Param(bool, "Fail if the host name doesn't match the " "gateway's host name", @@ -878,6 +919,7 @@ else: gw_group: Optional[str] = None, server_address: Optional[str] = None, secure: Optional[bool] = False, + force: Optional[bool] = False, verify_host_name: Optional[bool] = False, ): client = NVMeoFClient( @@ -892,6 +934,7 @@ else: trsvcid=int(trsvcid) if trsvcid is not None else None, adrfam=int(adrfam), secure=str_to_bool(secure), + force=str_to_bool(force), verify_host_name=str_to_bool(verify_host_name), ) ) @@ -919,6 +962,15 @@ else: "traddr": Param(str, "NVMeoF transport address"), "trsvcid": Param(int, "NVMeoF transport service port"), "adrfam": Param(int, "NVMeoF address family (0 - IPv4, 1 - IPv6)", True, 0), + "force": Param( + bool, + ( + "Delete listener even if there are active connections " + "or host name doesn't match" + ), + True, + False + ), "gw_group": Param(str, "NVMeoF gateway group", True, None), "server_address": Param(str, "NVMeoF gateway address", True, None), }, @@ -963,6 +1015,7 @@ else: parameters={ "nqn": Param(str, "NVMeoF subsystem NQN", True, None), "nsid": Param(str, "NVMeoF Namespace ID to filter by", True, None), + "uuid": Param(str, "NVMeoF Namespace UUID to filter by", True, None), "gw_group": Param(str, "NVMeoF gateway group", True, None), "server_address": Param(str, "NVMeoF gateway address", True, None), "traddr": Param(str, "NVMeoF gateway address (deprecated)", True, None), @@ -971,6 +1024,7 @@ else: @convert_to_model(model.NamespaceList) @handle_nvmeof_error def list(self, nqn: Optional[str] = None, nsid: Optional[str] = None, + uuid: Optional[str] = None, gw_group: Optional[str] = None, server_address: Optional[str] = None, traddr: Optional[str] = None): server_address = resolve_nvmeof_server_address( @@ -982,7 +1036,8 @@ else: server_address=server_address ).stub.list_namespaces( NVMeoFClient.pb2.list_namespaces_req(subsystem=nqn, - nsid=int(nsid) if nsid else None) + nsid=int(nsid) if nsid else None, + uuid=uuid) ) @pick("namespaces", first=True) @@ -1055,7 +1110,8 @@ else: "rados_namespace": Param(str, "RADOS namespace name", True, None), "rbd_pool": Param(str, "RBD pool name"), "rbd_data_pool": Param(str, "RBD data pool name", True, None), - "nsid": Param(str, "Create RBD image", True, None), + "nsid": Param(str, "Namespace ID", True, None), + "uuid": Param(str, "UUID", True, None), "create_image": Param(bool, "Create RBD image"), "size": Param(int, "Deprecated. Use `rbd_image_size` instead"), "rbd_image_size": Param(int, "RBD image size"), @@ -1098,6 +1154,7 @@ else: rbd_pool: str = "rbd", rbd_data_pool: Optional[str] = None, nsid: Optional[str] = None, + uuid: Optional[str] = None, create_image: Optional[bool] = False, size: Optional[int] = None, rbd_image_size: Optional[int] = None, @@ -1143,6 +1200,7 @@ else: NVMeoFClient.pb2.namespace_add_req( subsystem_nqn=nqn, nsid=int(nsid) if nsid else None, + uuid=uuid, rbd_image_name=rbd_image_name, rados_namespace_name=rados_namespace, rbd_pool_name=rbd_pool, @@ -1176,6 +1234,8 @@ else: "rbd_data_pool": Param(str, "RBD data pool name", True, None), "rados_namespace": Param(str, "RADOS namespace name", True, None), "rbd_image_name": Param(str, "RBD image name"), + "nsid": Param(str, "Namespace ID", True, None), + "uuid": Param(str, "UUID", True, None), "create_image": Param(bool, "Create RBD image"), "size": Param(str, "Deprecated. Use `rbd_image_size` instead", True, None), "rbd_image_size": Param(str, "RBD image size", True, None), @@ -1218,6 +1278,7 @@ else: rbd_pool: str = "rbd", rbd_data_pool: Optional[str] = None, nsid: Optional[str] = None, + uuid: Optional[str] = None, create_image: Optional[bool] = False, size: Optional[str] = None, rbd_image_size: Optional[str] = None, @@ -1278,6 +1339,7 @@ else: NVMeoFClient.pb2.namespace_add_req( subsystem_nqn=nqn, nsid=int(nsid) if nsid else None, + uuid=uuid, rbd_image_name=rbd_image_name, rados_namespace_name=rados_namespace, rbd_pool_name=rbd_pool, @@ -1638,6 +1700,170 @@ else: ) ) + @ReadPermission + @Endpoint('PUT', '{nsid}/change_location') + @NvmeofCLICommand( + "nvmeof namespace change_location", + model=model.RequestStatus, + alias="nvmeof ns change_location", + success_message_template=( + 'Setting location for namespace {nsid} in {nqn} to "{location}": Successful' + ) + ) + @EndpointDoc( + "Change the location of the specified NVMeoF namespace", + parameters={ + "nqn": Param(str, "NVMeoF subsystem NQN"), + "nsid": Param(str, "NVMeoF Namespace ID"), + "location": Param(str, "Gateway location for namespace"), + "gw_group": Param(str, "NVMeoF gateway group", True, None), + "server_address": Param(str, "NVMeoF gateway address", True, None), + "traddr": Param(str, "NVMeoF gateway address (deprecated)", True, None), + }, + ) + @convert_to_model(model.RequestStatus) + @handle_nvmeof_error + def change_location( + self, + nqn: str, + nsid: str, + location: str, + gw_group: Optional[str] = None, + server_address: Optional[str] = None, + traddr: Optional[str] = None + ): + server_address = resolve_nvmeof_server_address( + server_address=server_address, + traddr=traddr + ) + return NVMeoFClient( + gw_group=gw_group, + server_address=server_address + ).stub.namespace_change_location( + NVMeoFClient.pb2.namespace_change_location_req( + subsystem_nqn=nqn, + nsid=int(nsid), + location=location, + ) + ) + + @ReadPermission + @Endpoint('GET', 'list_hosts') + @NvmeofCLICommand( + "nvmeof namespace list_hosts", + model=model.NamespaceHostsList, + alias="nvmeof ns list_hosts" + ) + @EndpointDoc( + "List all NVMeoF namespaces with their allowed hosts", + parameters={ + "nqn": Param(str, "NVMeoF subsystem NQN", True, None), + "nsid": Param(str, "NVMeoF Namespace ID to filter by", True, None), + "uuid": Param(str, "NVMeoF Namespace UUID to filter by", True, None), + "gw_group": Param(str, "NVMeoF gateway group", True, None), + "server_address": Param(str, "NVMeoF gateway address", True, None), + "traddr": Param(str, "NVMeoF gateway address (deprecated)", True, None), + }, + ) + @handle_nvmeof_error + def list_hosts( + self, nqn: Optional[str] = None, nsid: Optional[str] = None, + uuid: Optional[str] = None, + gw_group: Optional[str] = None, server_address: Optional[str] = None, + traddr: Optional[str] = None + ): + server_address = resolve_nvmeof_server_address( + server_address=server_address, + traddr=traddr + ) + ns_list = NVMeoFClient( + gw_group=gw_group, + server_address=server_address + ).stub.list_namespaces( + NVMeoFClient.pb2.list_namespaces_req(subsystem=nqn, + nsid=int(nsid) if nsid else None, + uuid=uuid) + ) + + # Transform to NamedTuple with only NQN, NSID, and Hosts + host_infos = [] + for ns in ns_list.namespaces: + host_infos.append(model.NamespaceHostInfo( + nqn=ns.ns_subsystem_nqn or nqn or "", + nsid=ns.nsid, + hosts=list(ns.hosts) + )) + + return model.NamespaceHostsList( + status=ns_list.status, + error_message=ns_list.error_message, + namespaces=host_infos + ) + + @ReadPermission + @Endpoint('GET', 'list_locations') + @NvmeofCLICommand( + "nvmeof namespace list_locations", + model=model.NamespaceLocationsList, + alias="nvmeof ns list_locations" + ) + @EndpointDoc( + "List namespace distribution per site locations", + parameters={ + "nqn": Param(str, "NVMeoF subsystem NQN", True, None), + "nsid": Param(str, "NVMeoF Namespace ID to filter by", True, None), + "uuid": Param(str, "NVMeoF Namespace UUID to filter by", True, None), + "gw_group": Param(str, "NVMeoF gateway group", True, None), + "server_address": Param(str, "NVMeoF gateway address", True, None), + "traddr": Param(str, "NVMeoF gateway address (deprecated)", True, None), + }, + ) + @handle_nvmeof_error + def list_locations( + self, nqn: Optional[str] = None, nsid: Optional[str] = None, + uuid: Optional[str] = None, + gw_group: Optional[str] = None, server_address: Optional[str] = None, + traddr: Optional[str] = None + ): + server_address = resolve_nvmeof_server_address( + server_address=server_address, + traddr=traddr + ) + ns_list = NVMeoFClient( + gw_group=gw_group, + server_address=server_address + ).stub.list_namespaces( + NVMeoFClient.pb2.list_namespaces_req(subsystem=nqn, + nsid=int(nsid) if nsid else None, + uuid=uuid) + ) + + # Aggregate namespaces by subsystem, load balancing group, and location + location_counts: Dict[Tuple[str, int, str], int] = {} + for ns in ns_list.namespaces: + subsystem_nqn = ns.ns_subsystem_nqn or nqn or "" + lb_group = ns.load_balancing_group if ns.load_balancing_group else 0 + location = ns.location if ns.location else "" + + key = (subsystem_nqn, lb_group, location) + location_counts[key] = location_counts.get(key, 0) + 1 + + # Convert to NamedTuple list + location_infos = [] + for (subsystem_nqn, lb_group, location), count in sorted(location_counts.items()): + location_infos.append(model.NamespaceLocationInfo( + subsystem=subsystem_nqn, + load_balancing_group=lb_group, + location=location, + namespace_count=count + )) + + return model.NamespaceLocationsList( + status=ns_list.status, + error_message=ns_list.error_message, + locations=location_infos + ) + @ReadPermission @Endpoint('PUT', '{nsid}/set_auto_resize') @NvmeofCLICommand( @@ -2064,6 +2290,10 @@ else: parameters={ "nqn": Param(str, "NVMeoF subsystem NQN"), "host_nqn": Param(str, 'NVMeoF host NQN. Use "*" to disallow any host.'), + "force": Param( + bool, + "Delete the host even if it used in a namespace netmask", + True, False), "gw_group": Param(str, "NVMeoF gateway group", True, None), "server_address": Param(str, "NVMeoF gateway address", True, None), "traddr": Param(str, "NVMeoF gateway address (deprecated)", True, None), @@ -2071,7 +2301,8 @@ else: ) @convert_to_model(model.RequestStatus) @handle_nvmeof_error - def delete(self, nqn: str, host_nqn: str, gw_group: Optional[str] = None, + def delete(self, nqn: str, host_nqn: str, force: Optional[bool] = False, + gw_group: Optional[str] = None, server_address: Optional[str] = None, traddr: Optional[str] = None): server_address = resolve_nvmeof_server_address( @@ -2082,7 +2313,7 @@ else: gw_group=gw_group, server_address=server_address ).stub.remove_host( - NVMeoFClient.pb2.remove_host_req(subsystem_nqn=nqn, host_nqn=host_nqn) + NVMeoFClient.pb2.remove_host_req(subsystem_nqn=nqn, host_nqn=host_nqn, force=force) ) @empty_response diff --git a/src/pybind/mgr/dashboard/model/nvmeof.py b/src/pybind/mgr/dashboard/model/nvmeof.py index d832eb26140..f4b7327514e 100644 --- a/src/pybind/mgr/dashboard/model/nvmeof.py +++ b/src/pybind/mgr/dashboard/model/nvmeof.py @@ -207,6 +207,35 @@ class NamespaceList(NamedTuple): namespaces: Annotated[List[Namespace], CliFlags.EXCLUSIVE_LIST] +class NamespaceHostInfo(NamedTuple): + nqn: Annotated[str, CliHeader("NQN")] + nsid: Annotated[int, CliHeader("NSID")] + hosts: Annotated[ + List[str], + CliHeader("Hosts"), + CliFieldTransformer(lambda v: "\n".join(v) if v else "None") + ] + + +class NamespaceHostsList(NamedTuple): + status: int + error_message: str + namespaces: Annotated[List[NamespaceHostInfo], CliFlags.EXCLUSIVE_LIST] + + +class NamespaceLocationInfo(NamedTuple): + subsystem: Annotated[str, CliHeader("Subsystem")] + load_balancing_group: Annotated[int, CliHeader("Load Balancing Group")] + location: Annotated[str, CliHeader("Location")] + namespace_count: Annotated[int, CliHeader("Count")] + + +class NamespaceLocationsList(NamedTuple): + status: int + error_message: str + locations: Annotated[List[NamespaceLocationInfo], CliFlags.EXCLUSIVE_LIST] + + class NamespaceIOStats(NamedTuple): status: Annotated[int, CliFlags.DROP] error_message: Annotated[str, CliFlags.DROP] @@ -292,6 +321,19 @@ class GatewayStatsInfo(NamedTuple): poll_groups: Annotated[List[PollGroupInfo], CliFlags.EXCLUSIVE_LIST] +class SpdkThreadInfo(NamedTuple): + name: str + busy: int + idle: int + + +class ThreadStatsInfo(NamedTuple): + status: int + error_message: str + tick_rate: int + threads: Annotated[List[SpdkThreadInfo], CliFlags.EXCLUSIVE_LIST] + + class AnaState(Enum): UNSET = 0 OPTIMIZED = 1 diff --git a/src/pybind/mgr/dashboard/openapi.yaml b/src/pybind/mgr/dashboard/openapi.yaml index abb7a7351ff..2ddd1190b47 100644 --- a/src/pybind/mgr/dashboard/openapi.yaml +++ b/src/pybind/mgr/dashboard/openapi.yaml @@ -12961,6 +12961,51 @@ paths: summary: Get NVMeoF statistics for the gateway tags: - NVMe-oF Gateway + /api/nvmeof/gateway/thread_stats: + get: + parameters: + - allowEmptyValue: true + description: NVMeoF gateway group + in: query + name: gw_group + schema: + type: string + - allowEmptyValue: true + description: NVMeoF gateway address + in: query + name: server_address + schema: + type: string + - allowEmptyValue: true + description: NVMeoF gateway address (deprecated) + in: query + name: traddr + 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: Get NVMeoF thread statistics for the gateway + tags: + - NVMe-oF Gateway /api/nvmeof/gateway/version: get: parameters: @@ -13177,6 +13222,18 @@ paths: /api/nvmeof/subsystem: get: parameters: + - allowEmptyValue: true + description: NVMeoF subsystem NQN to filter by + in: query + name: nqn + schema: + type: string + - allowEmptyValue: true + description: Serial number to filter by + in: query + name: serial_number + schema: + type: string - allowEmptyValue: true description: NVMeoF gateway group in: query @@ -13599,6 +13656,12 @@ paths: required: true schema: type: string + - default: false + description: Delete the host even if it used in a namespace netmask + in: query + name: force + schema: + type: boolean - allowEmptyValue: true description: NVMeoF gateway group in: query @@ -13980,6 +14043,10 @@ paths: default: 0 description: NVMeoF address family (0 - IPv4, 1 - IPv6) type: integer + force: + default: false + description: Allow contradiction in security with existing listeners + type: boolean gw_group: description: NVMeoF gateway group type: string @@ -14075,6 +14142,8 @@ paths: schema: type: integer - default: false + description: Delete listener even if there are active connections or host + name doesn't match in: query name: force schema: @@ -14139,6 +14208,12 @@ paths: name: nsid schema: type: string + - allowEmptyValue: true + description: NVMeoF Namespace UUID to filter by + in: query + name: uuid + schema: + type: string - allowEmptyValue: true description: NVMeoF gateway group in: query @@ -14240,7 +14315,7 @@ paths: description: Namespace will be visible only for the allowed hosts type: boolean nsid: - description: Create RBD image + description: Namespace ID type: string rados_namespace: description: RADOS namespace name @@ -14275,6 +14350,9 @@ paths: default: false description: Trash the RBD image when namespace is removed type: boolean + uuid: + description: UUID + type: string required: - rbd_image_name type: object @@ -14311,6 +14389,132 @@ paths: summary: Create a new NVMeoF namespace. tags: - NVMe-oF Subsystem Namespace + /api/nvmeof/subsystem/{nqn}/namespace/list_hosts: + get: + parameters: + - allowEmptyValue: true + description: NVMeoF subsystem NQN + in: path + name: nqn + schema: + type: string + - allowEmptyValue: true + description: NVMeoF Namespace ID to filter by + in: query + name: nsid + schema: + type: string + - allowEmptyValue: true + description: NVMeoF Namespace UUID to filter by + in: query + name: uuid + schema: + type: string + - allowEmptyValue: true + description: NVMeoF gateway group + in: query + name: gw_group + schema: + type: string + - allowEmptyValue: true + description: NVMeoF gateway address + in: query + name: server_address + schema: + type: string + - allowEmptyValue: true + description: NVMeoF gateway address (deprecated) + in: query + name: traddr + 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 with their allowed hosts + tags: + - NVMe-oF Subsystem Namespace + /api/nvmeof/subsystem/{nqn}/namespace/list_locations: + get: + parameters: + - allowEmptyValue: true + description: NVMeoF subsystem NQN + in: path + name: nqn + schema: + type: string + - allowEmptyValue: true + description: NVMeoF Namespace ID to filter by + in: query + name: nsid + schema: + type: string + - allowEmptyValue: true + description: NVMeoF Namespace UUID to filter by + in: query + name: uuid + schema: + type: string + - allowEmptyValue: true + description: NVMeoF gateway group + in: query + name: gw_group + schema: + type: string + - allowEmptyValue: true + description: NVMeoF gateway address + in: query + name: server_address + schema: + type: string + - allowEmptyValue: true + description: NVMeoF gateway address (deprecated) + in: query + name: traddr + 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 namespace distribution per site locations + tags: + - NVMe-oF Subsystem Namespace /api/nvmeof/subsystem/{nqn}/namespace/{nsid}: delete: parameters: @@ -14654,6 +14858,74 @@ paths: summary: set the load balancing group for specified NVMeoF namespace tags: - NVMe-oF Subsystem Namespace + /api/nvmeof/subsystem/{nqn}/namespace/{nsid}/change_location: + put: + parameters: + - description: NVMeoF subsystem NQN + in: path + name: nqn + required: true + schema: + type: string + - description: NVMeoF Namespace ID + in: path + name: nsid + required: true + schema: + type: string + requestBody: + content: + application/json: + schema: + properties: + gw_group: + description: NVMeoF gateway group + type: string + location: + description: Gateway location for namespace + type: string + server_address: + description: NVMeoF gateway address + type: string + traddr: + description: NVMeoF gateway address (deprecated) + type: string + required: + - location + type: object + responses: + '200': + content: + application/json: + schema: + type: object + application/vnd.ceph.api.v1.0+json: + schema: + type: object + description: Resource updated. + '202': + content: + application/json: + schema: + type: object + application/vnd.ceph.api.v1.0+json: + schema: + type: object + description: Operation is still executing. Please check the task queue. + '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: Change the location of the specified NVMeoF namespace + tags: + - NVMe-oF Subsystem Namespace /api/nvmeof/subsystem/{nqn}/namespace/{nsid}/change_visibility: put: parameters: