Skip to content

Commit

Permalink
Merge pull request ceph#61392 from Hezko/nvmeof-gw-info-cli
Browse files Browse the repository at this point in the history
Dashboard: Introduce nvmeof cli commands
  • Loading branch information
Hezko authored Jan 27, 2025
2 parents 1051088 + 6e0ae19 commit fe3413f
Show file tree
Hide file tree
Showing 5 changed files with 151 additions and 4 deletions.
21 changes: 20 additions & 1 deletion src/pybind/mgr/dashboard/controllers/nvmeof.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from .. import mgr
from ..model import nvmeof as model
from ..security import Scope
from ..services.nvmeof_cli import NvmeofCLICommand
from ..services.orchestrator import OrchClient
from ..tools import str_to_bool
from . import APIDoc, APIRouter, BaseController, CreatePermission, \
Expand All @@ -30,6 +31,7 @@
@APIDoc("NVMe-oF Gateway Management API", "NVMe-oF Gateway")
class NVMeoFGateway(RESTController):
@EndpointDoc("Get information about the NVMeoF gateway")
@NvmeofCLICommand("nvmeof gw info")
@map_model(model.GatewayInfo)
@handle_nvmeof_error
def list(self, gw_group: Optional[str] = None):
Expand All @@ -54,6 +56,7 @@ def group(self):
@APIDoc("NVMe-oF Subsystem Management API", "NVMe-oF Subsystem")
class NVMeoFSubsystem(RESTController):
@EndpointDoc("List all NVMeoF subsystems")
@NvmeofCLICommand("nvmeof subsystem list")
@map_collection(model.Subsystem, pick="subsystems")
@handle_nvmeof_error
def list(self, gw_group: Optional[str] = None):
Expand All @@ -68,6 +71,7 @@ def list(self, gw_group: Optional[str] = None):
"gw_group": Param(str, "NVMeoF gateway group", True, None),
},
)
@NvmeofCLICommand("nvmeof subsystem get")
@map_model(model.Subsystem, first="subsystems")
@handle_nvmeof_error
def get(self, nqn: str, gw_group: Optional[str] = None):
Expand All @@ -84,6 +88,7 @@ def get(self, nqn: str, gw_group: Optional[str] = None):
"gw_group": Param(str, "NVMeoF gateway group", True, None),
},
)
@NvmeofCLICommand("nvmeof subsystem add")
@empty_response
@handle_nvmeof_error
def create(self, nqn: str, enable_ha: bool, max_namespaces: int = 1024,
Expand All @@ -98,10 +103,11 @@ def create(self, nqn: str, enable_ha: bool, max_namespaces: int = 1024,
"Delete an existing NVMeoF subsystem",
parameters={
"nqn": Param(str, "NVMeoF subsystem NQN"),
"force": Param(bool, "Force delete", "false"),
"force": Param(bool, "Force delete", True, False),
"gw_group": Param(str, "NVMeoF gateway group", True, None),
},
)
@NvmeofCLICommand("nvmeof subsystem del")
@empty_response
@handle_nvmeof_error
def delete(self, nqn: str, force: Optional[str] = "false", gw_group: Optional[str] = None):
Expand All @@ -121,6 +127,7 @@ class NVMeoFListener(RESTController):
"gw_group": Param(str, "NVMeoF gateway group", True, None),
},
)
@NvmeofCLICommand("nvmeof listener list")
@map_collection(model.Listener, pick="listeners")
@handle_nvmeof_error
def list(self, nqn: str, gw_group: Optional[str] = None):
Expand All @@ -139,6 +146,7 @@ def list(self, nqn: str, gw_group: Optional[str] = None):
"gw_group": Param(str, "NVMeoF gateway group", True, None),
},
)
@NvmeofCLICommand("nvmeof listener add")
@empty_response
@handle_nvmeof_error
def create(
Expand Down Expand Up @@ -171,6 +179,7 @@ def create(
"gw_group": Param(str, "NVMeoF gateway group", True, None),
},
)
@NvmeofCLICommand("nvmeof listener del")
@empty_response
@handle_nvmeof_error
def delete(
Expand Down Expand Up @@ -204,6 +213,7 @@ class NVMeoFNamespace(RESTController):
"gw_group": Param(str, "NVMeoF gateway group", True, None),
},
)
@NvmeofCLICommand("nvmeof ns list")
@map_collection(model.Namespace, pick="namespaces")
@handle_nvmeof_error
def list(self, nqn: str, gw_group: Optional[str] = None):
Expand All @@ -219,6 +229,7 @@ def list(self, nqn: str, gw_group: Optional[str] = None):
"gw_group": Param(str, "NVMeoF gateway group", True, None),
},
)
@NvmeofCLICommand("nvmeof ns get")
@map_model(model.Namespace, first="namespaces")
@handle_nvmeof_error
def get(self, nqn: str, nsid: str, gw_group: Optional[str] = None):
Expand All @@ -236,6 +247,7 @@ def get(self, nqn: str, nsid: str, gw_group: Optional[str] = None):
"gw_group": Param(str, "NVMeoF gateway group", True, None),
},
)
@NvmeofCLICommand("nvmeof ns get_io_stats")
@map_model(model.NamespaceIOStats)
@handle_nvmeof_error
def io_stats(self, nqn: str, nsid: str, gw_group: Optional[str] = None):
Expand All @@ -257,6 +269,7 @@ def io_stats(self, nqn: str, nsid: str, gw_group: Optional[str] = None):
"gw_group": Param(str, "NVMeoF gateway group", True, None),
},
)
@NvmeofCLICommand("nvmeof ns add")
@map_model(model.NamespaceCreation)
@handle_nvmeof_error
def create(
Expand Down Expand Up @@ -296,6 +309,7 @@ def create(
"gw_group": Param(str, "NVMeoF gateway group", True, None),
},
)
@NvmeofCLICommand("nvmeof ns update")
@empty_response
@handle_nvmeof_error
def update(
Expand Down Expand Up @@ -360,6 +374,7 @@ def update(
"gw_group": Param(str, "NVMeoF gateway group", True, None),
},
)
@NvmeofCLICommand("nvmeof ns del")
@empty_response
@handle_nvmeof_error
def delete(self, nqn: str, nsid: str, gw_group: Optional[str] = None):
Expand All @@ -378,6 +393,7 @@ class NVMeoFHost(RESTController):
"gw_group": Param(str, "NVMeoF gateway group", True, None),
},
)
@NvmeofCLICommand("nvmeof host list")
@map_collection(
model.Host,
pick="hosts",
Expand All @@ -400,6 +416,7 @@ def list(self, nqn: str, gw_group: Optional[str] = None):
"gw_group": Param(str, "NVMeoF gateway group", True, None),
},
)
@NvmeofCLICommand("nvmeof host add")
@empty_response
@handle_nvmeof_error
def create(self, nqn: str, host_nqn: str, gw_group: Optional[str] = None):
Expand All @@ -415,6 +432,7 @@ def create(self, nqn: str, host_nqn: str, gw_group: Optional[str] = None):
"gw_group": Param(str, "NVMeoF gateway group", True, None),
},
)
@NvmeofCLICommand("nvmeof host del")
@empty_response
@handle_nvmeof_error
def delete(self, nqn: str, host_nqn: str, gw_group: Optional[str] = None):
Expand All @@ -432,6 +450,7 @@ class NVMeoFConnection(RESTController):
"gw_group": Param(str, "NVMeoF gateway group", True, None),
},
)
@NvmeofCLICommand("nvmeof connection list")
@map_collection(model.Connection, pick="connections")
@handle_nvmeof_error
def list(self, nqn: str, gw_group: Optional[str] = None):
Expand Down
1 change: 1 addition & 0 deletions src/pybind/mgr/dashboard/module.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
create_self_signed_cert, get_default_addr, verify_tls_files

from . import mgr
from .controllers import nvmeof # noqa # pylint: disable=unused-import
from .controllers import Router, json_error_page
from .grafana import push_local_dashboards
from .services import nvmeof_cli # noqa # pylint: disable=unused-import
Expand Down
42 changes: 41 additions & 1 deletion src/pybind/mgr/dashboard/services/nvmeof_cli.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
# -*- coding: utf-8 -*-
import errno
import json
from typing import Any, Dict, Optional

from mgr_module import CLICheckNonemptyFileInput, CLIReadCommand, CLIWriteCommand
import yaml
from mgr_module import CLICheckNonemptyFileInput, CLICommand, CLIReadCommand, \
CLIWriteCommand, HandleCommandResult, HandlerFuncType

from ..exceptions import DashboardException
from ..rest_client import RequestException
from .nvmeof_conf import ManagedByOrchestratorException, \
NvmeofGatewayAlreadyExists, NvmeofGatewaysConfig
Expand Down Expand Up @@ -45,3 +49,39 @@ def remove_nvmeof_gateway(_, name: str, daemon_name: str = ''):
return 0, 'Success', ''
except ManagedByOrchestratorException as ex:
return -errno.EINVAL, '', str(ex)


class NvmeofCLICommand(CLICommand):
def __call__(self, func) -> HandlerFuncType: # type: ignore
# pylint: disable=useless-super-delegation
"""
This method is being overriden solely to be able to disable the linters checks for typing.
The NvmeofCLICommand decorator assumes a different type returned from the
function it wraps compared to CLICmmand, breaking a Liskov substitution principal,
hence triggering linters alerts.
"""
return super().__call__(func)

def call(self,
mgr: Any,
cmd_dict: Dict[str, Any],
inbuf: Optional[str] = None) -> HandleCommandResult:
try:
ret = super().call(mgr, cmd_dict, inbuf)
out_format = cmd_dict.get('format')
if out_format == 'json' or not out_format:
if ret is None:
out = ''
else:
out = json.dumps(ret)
elif out_format == 'yaml':
if ret is None:
out = ''
else:
out = yaml.dump(ret)
else:
return HandleCommandResult(-errno.EINVAL, '',
f"format '{out_format}' is not implemented")
return HandleCommandResult(0, out, '')
except DashboardException as e:
return HandleCommandResult(-errno.EINVAL, '', str(e))
4 changes: 2 additions & 2 deletions src/pybind/mgr/dashboard/services/nvmeof_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@
import grpc._channel # type: ignore
from google.protobuf.message import Message # type: ignore

from .proto import gateway_pb2 as pb2
from .proto import gateway_pb2_grpc as pb2_grpc
from .proto import gateway_pb2 as pb2 # type: ignore
from .proto import gateway_pb2_grpc as pb2_grpc # type: ignore
except ImportError:
grpc = None
else:
Expand Down
87 changes: 87 additions & 0 deletions src/pybind/mgr/dashboard/tests/test_nvmeof_cli.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import errno
from unittest.mock import MagicMock

import pytest
from mgr_module import CLICommand, HandleCommandResult

from ..services.nvmeof_cli import NvmeofCLICommand


@pytest.fixture(scope="class", name="sample_command")
def fixture_sample_command():
test_cmd = "test command"

@NvmeofCLICommand(test_cmd)
def func(_): # noqa # pylint: disable=unused-variable
return {'a': '1', 'b': 2}
yield test_cmd
del NvmeofCLICommand.COMMANDS[test_cmd]
assert test_cmd not in NvmeofCLICommand.COMMANDS


@pytest.fixture(name='base_call_mock')
def fixture_base_call_mock(monkeypatch):
mock_result = {'a': 'b'}
super_mock = MagicMock()
super_mock.return_value = mock_result
monkeypatch.setattr(CLICommand, 'call', super_mock)
return super_mock


@pytest.fixture(name='base_call_return_none_mock')
def fixture_base_call_return_none_mock(monkeypatch):
mock_result = None
super_mock = MagicMock()
super_mock.return_value = mock_result
monkeypatch.setattr(CLICommand, 'call', super_mock)
return super_mock


class TestNvmeofCLICommand:
def test_command_being_added(self, sample_command):
assert sample_command in NvmeofCLICommand.COMMANDS
assert isinstance(NvmeofCLICommand.COMMANDS[sample_command], NvmeofCLICommand)

def test_command_return_cmd_result_default_format(self, base_call_mock, sample_command):
result = NvmeofCLICommand.COMMANDS[sample_command].call(MagicMock(), {})
assert isinstance(result, HandleCommandResult)
assert result.retval == 0
assert result.stdout == '{"a": "b"}'
assert result.stderr == ''
base_call_mock.assert_called_once()

def test_command_return_cmd_result_json_format(self, base_call_mock, sample_command):
result = NvmeofCLICommand.COMMANDS[sample_command].call(MagicMock(), {'format': 'json'})
assert isinstance(result, HandleCommandResult)
assert result.retval == 0
assert result.stdout == '{"a": "b"}'
assert result.stderr == ''
base_call_mock.assert_called_once()

def test_command_return_cmd_result_yaml_format(self, base_call_mock, sample_command):
result = NvmeofCLICommand.COMMANDS[sample_command].call(MagicMock(), {'format': 'yaml'})
assert isinstance(result, HandleCommandResult)
assert result.retval == 0
assert result.stdout == 'a: b\n'
assert result.stderr == ''
base_call_mock.assert_called_once()

def test_command_return_cmd_result_invalid_format(self, base_call_mock, sample_command):
mock_result = {'a': 'b'}
super_mock = MagicMock()
super_mock.call.return_value = mock_result

result = NvmeofCLICommand.COMMANDS[sample_command].call(MagicMock(), {'format': 'invalid'})
assert isinstance(result, HandleCommandResult)
assert result.retval == -errno.EINVAL
assert result.stdout == ''
assert result.stderr
base_call_mock.assert_called_once()

def test_command_return_empty_cmd_result(self, base_call_return_none_mock, sample_command):
result = NvmeofCLICommand.COMMANDS[sample_command].call(MagicMock(), {})
assert isinstance(result, HandleCommandResult)
assert result.retval == 0
assert result.stdout == ''
assert result.stderr == ''
base_call_return_none_mock.assert_called_once()

0 comments on commit fe3413f

Please sign in to comment.