]> git.apps.os.sepia.ceph.com Git - ceph.git/commitdiff
mgr/dashboard: remove map_collection decorator and adapt host list nvme endpoint 64231/head
authorTomer Haskalovitch <il033030@tomers-mbp.givatayim.il.ibm.com>
Mon, 21 Apr 2025 09:48:00 +0000 (12:48 +0300)
committerHezko <tomer.haska@gmail.com>
Sat, 28 Jun 2025 19:39:50 +0000 (22:39 +0300)
Signed-off-by: Tomer Haskalovitch <il033030@tomers-mbp.givatayim.il.ibm.com>
(cherry picked from commit 63233196aee12a76203fd168e1cfb1cadaafdd21)

src/pybind/mgr/dashboard/controllers/nvmeof.py
src/pybind/mgr/dashboard/model/nvmeof.py
src/pybind/mgr/dashboard/services/nvmeof_client.py
src/pybind/mgr/dashboard/tests/test_nvmeof_client.py

index b75a335bdc6fc789ae6796b3ae6b7528a9cb2895..642c939a15f13427519aeb806ae3ce60f298b077 100644 (file)
@@ -24,7 +24,7 @@ NVME_SCHEMA = {
 
 try:
     from ..services.nvmeof_client import NVMeoFClient, convert_to_model, \
-        empty_response, handle_nvmeof_error, map_collection, pick
+        empty_response, handle_nvmeof_error, pick
 except ImportError as e:
     logger.error("Failed to import NVMeoFClient and related components: %s", e)
 else:
@@ -529,6 +529,11 @@ else:
                 )
             )
 
+    def _update_hosts(hosts_info_resp):
+        if hosts_info_resp.get('allow_any_host'):
+            hosts_info_resp['hosts'].insert(0, {"nqn": "*"})
+        return hosts_info_resp
+
     @APIRouter("/nvmeof/subsystem/{nqn}/host", Scope.NVME_OF)
     @APIDoc("NVMe-oF Subsystem Host Allowlist Management API",
             "NVMe-oF Subsystem Host Allowlist")
@@ -540,15 +545,9 @@ else:
                 "gw_group": Param(str, "NVMeoF gateway group", True, None),
             },
         )
+        @pick('hosts')
         @NvmeofCLICommand("nvmeof host list")
-        @map_collection(
-            model.Host,
-            pick="hosts",
-            # Display the "allow any host" option as another host item
-            finalize=lambda i, o: [model.Host(nqn="*")._asdict()] + o
-            if i.allow_any_host
-            else o,
-        )
+        @convert_to_model(model.HostsInfo, finalize=_update_hosts)
         @handle_nvmeof_error
         def list(self, nqn: str, gw_group: Optional[str] = None, traddr: Optional[str] = None):
             return NVMeoFClient(gw_group=gw_group, traddr=traddr).stub.list_hosts(
index 49b175a02e1ec06dd43291f35fc0f34935801eb2..268106247a8bd1c797446d45c7d08b5f2dc4c480 100644 (file)
@@ -140,6 +140,14 @@ class Host(NamedTuple):
     nqn: str
 
 
+class HostsInfo(NamedTuple):
+    status: int
+    error_message: str
+    allow_any_host: bool
+    subsystem_nqn: str
+    hosts: List[Host]
+
+
 class RequestStatus(NamedTuple):
     status: int
     error_message: str
index 556b59eb3655444e740a12deb738bf14af8f1e96..79b4a607ecf2c001662ff86da033100feb67a5f0 100644 (file)
@@ -1,6 +1,5 @@
 import functools
 import logging
-from collections.abc import Iterable
 from typing import Any, Callable, Dict, Generator, List, NamedTuple, Optional, Type
 
 from ..exceptions import DashboardException
@@ -72,62 +71,9 @@ else:
                 self.channel = grpc.insecure_channel(self.gateway_addr)
             self.stub = pb2_grpc.GatewayStub(self.channel)
 
-    def make_namedtuple_from_object(cls: Type[NamedTuple], obj: Any) -> NamedTuple:
-        return cls(
-            **{
-                field: getattr(obj, field)
-                for field in cls._fields
-                if hasattr(obj, field)
-            }
-        )  # type: ignore
-
     Model = Dict[str, Any]
-
-    def map_model(
-        model: Type[NamedTuple],
-        first: Optional[str] = None,
-    ) -> Callable[..., Callable[..., Model]]:
-        def decorator(func: Callable[..., Message]) -> Callable[..., Model]:
-            @functools.wraps(func)
-            def wrapper(*args, **kwargs) -> Model:
-                message = func(*args, **kwargs)
-                if first:
-                    try:
-                        message = getattr(message, first)[0]
-                    except IndexError:
-                        raise DashboardException(
-                            msg="Not Found", http_status_code=404, component="nvmeof"
-                        )
-
-                return make_namedtuple_from_object(model, message)._asdict()
-
-            return wrapper
-
-        return decorator
-
     Collection = List[Model]
 
-    def map_collection(
-        model: Type[NamedTuple],
-        pick: str,  # pylint: disable=redefined-outer-name
-        finalize: Optional[Callable[[Message, Collection], Collection]] = None,
-    ) -> Callable[..., Callable[..., Collection]]:
-        def decorator(func: Callable[..., Message]) -> Callable[..., Collection]:
-            @functools.wraps(func)
-            def wrapper(*args, **kwargs) -> Collection:
-                message = func(*args, **kwargs)
-                collection: Iterable = getattr(message, pick)
-                out = [
-                    make_namedtuple_from_object(model, i)._asdict() for i in collection
-                ]
-                if finalize:
-                    return finalize(message, out)
-                return out
-
-            return wrapper
-
-        return decorator
-
     import errno
 
     NVMeoFError2HTTP = {
@@ -266,21 +212,28 @@ else:
             ]
         return obj
 
-    def convert_to_model(model: Type[NamedTuple]) -> Callable[..., Callable[..., Model]]:
+    def convert_to_model(model: Type[NamedTuple],
+                         finalize: Optional[Callable[[Dict], Dict]] = None
+                         ) -> Callable[..., Callable[..., Model]]:
         def decorator(func: Callable[..., Message]) -> Callable[..., Model]:
             @functools.wraps(func)
             def wrapper(*args, **kwargs) -> Model:
                 message = func(*args, **kwargs)
                 msg_dict = MessageToDict(message, including_default_value_fields=True,
                                          preserving_proto_field_name=True)
-                return namedtuple_to_dict(obj_to_namedtuple(msg_dict, model))
+
+                result = namedtuple_to_dict(obj_to_namedtuple(msg_dict, model))
+                if finalize:
+                    return finalize(result)
+                return result
 
             return wrapper
 
         return decorator
 
     # pylint: disable-next=redefined-outer-name
-    def pick(field: str, first: bool = False) -> Callable[..., Callable[..., object]]:
+    def pick(field: str, first: bool = False,
+             ) -> Callable[..., Callable[..., object]]:
         def decorator(func: Callable[..., Dict]) -> Callable[..., object]:
             @functools.wraps(func)
             def wrapper(*args, **kwargs) -> object:
index 76fcd63f7cf506c28aa93649018c14037db463cf..6ffe5c323f762933614b9a5f5bc452f43077fd0c 100644 (file)
@@ -385,6 +385,18 @@ class TestConvertToModel:
         result = empty_func()
         assert result == {}
 
+    def test_finalize(self, disable_message_to_dict):
+        # pylint: disable=unused-argument
+        def finalizer(output):
+            output['name'] = output['name'].upper()
+            return output
+
+        @convert_to_model(Boy, finalize=finalizer)
+        def get_person() -> dict:
+            return {"name": "Alice", "age": 30}
+
+        assert get_person()['name'] == "ALICE"
+
 
 class TestPick:
     def test_basic_field_access(self):