Skip to content

Commit fe3413f

Browse files
authored
Merge pull request ceph#61392 from Hezko/nvmeof-gw-info-cli
Dashboard: Introduce nvmeof cli commands
2 parents 1051088 + 6e0ae19 commit fe3413f

File tree

5 files changed

+151
-4
lines changed

5 files changed

+151
-4
lines changed

src/pybind/mgr/dashboard/controllers/nvmeof.py

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from .. import mgr
88
from ..model import nvmeof as model
99
from ..security import Scope
10+
from ..services.nvmeof_cli import NvmeofCLICommand
1011
from ..services.orchestrator import OrchClient
1112
from ..tools import str_to_bool
1213
from . import APIDoc, APIRouter, BaseController, CreatePermission, \
@@ -30,6 +31,7 @@
3031
@APIDoc("NVMe-oF Gateway Management API", "NVMe-oF Gateway")
3132
class NVMeoFGateway(RESTController):
3233
@EndpointDoc("Get information about the NVMeoF gateway")
34+
@NvmeofCLICommand("nvmeof gw info")
3335
@map_model(model.GatewayInfo)
3436
@handle_nvmeof_error
3537
def list(self, gw_group: Optional[str] = None):
@@ -54,6 +56,7 @@ def group(self):
5456
@APIDoc("NVMe-oF Subsystem Management API", "NVMe-oF Subsystem")
5557
class NVMeoFSubsystem(RESTController):
5658
@EndpointDoc("List all NVMeoF subsystems")
59+
@NvmeofCLICommand("nvmeof subsystem list")
5760
@map_collection(model.Subsystem, pick="subsystems")
5861
@handle_nvmeof_error
5962
def list(self, gw_group: Optional[str] = None):
@@ -68,6 +71,7 @@ def list(self, gw_group: Optional[str] = None):
6871
"gw_group": Param(str, "NVMeoF gateway group", True, None),
6972
},
7073
)
74+
@NvmeofCLICommand("nvmeof subsystem get")
7175
@map_model(model.Subsystem, first="subsystems")
7276
@handle_nvmeof_error
7377
def get(self, nqn: str, gw_group: Optional[str] = None):
@@ -84,6 +88,7 @@ def get(self, nqn: str, gw_group: Optional[str] = None):
8488
"gw_group": Param(str, "NVMeoF gateway group", True, None),
8589
},
8690
)
91+
@NvmeofCLICommand("nvmeof subsystem add")
8792
@empty_response
8893
@handle_nvmeof_error
8994
def create(self, nqn: str, enable_ha: bool, max_namespaces: int = 1024,
@@ -98,10 +103,11 @@ def create(self, nqn: str, enable_ha: bool, max_namespaces: int = 1024,
98103
"Delete an existing NVMeoF subsystem",
99104
parameters={
100105
"nqn": Param(str, "NVMeoF subsystem NQN"),
101-
"force": Param(bool, "Force delete", "false"),
106+
"force": Param(bool, "Force delete", True, False),
102107
"gw_group": Param(str, "NVMeoF gateway group", True, None),
103108
},
104109
)
110+
@NvmeofCLICommand("nvmeof subsystem del")
105111
@empty_response
106112
@handle_nvmeof_error
107113
def delete(self, nqn: str, force: Optional[str] = "false", gw_group: Optional[str] = None):
@@ -121,6 +127,7 @@ class NVMeoFListener(RESTController):
121127
"gw_group": Param(str, "NVMeoF gateway group", True, None),
122128
},
123129
)
130+
@NvmeofCLICommand("nvmeof listener list")
124131
@map_collection(model.Listener, pick="listeners")
125132
@handle_nvmeof_error
126133
def list(self, nqn: str, gw_group: Optional[str] = None):
@@ -139,6 +146,7 @@ def list(self, nqn: str, gw_group: Optional[str] = None):
139146
"gw_group": Param(str, "NVMeoF gateway group", True, None),
140147
},
141148
)
149+
@NvmeofCLICommand("nvmeof listener add")
142150
@empty_response
143151
@handle_nvmeof_error
144152
def create(
@@ -171,6 +179,7 @@ def create(
171179
"gw_group": Param(str, "NVMeoF gateway group", True, None),
172180
},
173181
)
182+
@NvmeofCLICommand("nvmeof listener del")
174183
@empty_response
175184
@handle_nvmeof_error
176185
def delete(
@@ -204,6 +213,7 @@ class NVMeoFNamespace(RESTController):
204213
"gw_group": Param(str, "NVMeoF gateway group", True, None),
205214
},
206215
)
216+
@NvmeofCLICommand("nvmeof ns list")
207217
@map_collection(model.Namespace, pick="namespaces")
208218
@handle_nvmeof_error
209219
def list(self, nqn: str, gw_group: Optional[str] = None):
@@ -219,6 +229,7 @@ def list(self, nqn: str, gw_group: Optional[str] = None):
219229
"gw_group": Param(str, "NVMeoF gateway group", True, None),
220230
},
221231
)
232+
@NvmeofCLICommand("nvmeof ns get")
222233
@map_model(model.Namespace, first="namespaces")
223234
@handle_nvmeof_error
224235
def get(self, nqn: str, nsid: str, gw_group: Optional[str] = None):
@@ -236,6 +247,7 @@ def get(self, nqn: str, nsid: str, gw_group: Optional[str] = None):
236247
"gw_group": Param(str, "NVMeoF gateway group", True, None),
237248
},
238249
)
250+
@NvmeofCLICommand("nvmeof ns get_io_stats")
239251
@map_model(model.NamespaceIOStats)
240252
@handle_nvmeof_error
241253
def io_stats(self, nqn: str, nsid: str, gw_group: Optional[str] = None):
@@ -257,6 +269,7 @@ def io_stats(self, nqn: str, nsid: str, gw_group: Optional[str] = None):
257269
"gw_group": Param(str, "NVMeoF gateway group", True, None),
258270
},
259271
)
272+
@NvmeofCLICommand("nvmeof ns add")
260273
@map_model(model.NamespaceCreation)
261274
@handle_nvmeof_error
262275
def create(
@@ -296,6 +309,7 @@ def create(
296309
"gw_group": Param(str, "NVMeoF gateway group", True, None),
297310
},
298311
)
312+
@NvmeofCLICommand("nvmeof ns update")
299313
@empty_response
300314
@handle_nvmeof_error
301315
def update(
@@ -360,6 +374,7 @@ def update(
360374
"gw_group": Param(str, "NVMeoF gateway group", True, None),
361375
},
362376
)
377+
@NvmeofCLICommand("nvmeof ns del")
363378
@empty_response
364379
@handle_nvmeof_error
365380
def delete(self, nqn: str, nsid: str, gw_group: Optional[str] = None):
@@ -378,6 +393,7 @@ class NVMeoFHost(RESTController):
378393
"gw_group": Param(str, "NVMeoF gateway group", True, None),
379394
},
380395
)
396+
@NvmeofCLICommand("nvmeof host list")
381397
@map_collection(
382398
model.Host,
383399
pick="hosts",
@@ -400,6 +416,7 @@ def list(self, nqn: str, gw_group: Optional[str] = None):
400416
"gw_group": Param(str, "NVMeoF gateway group", True, None),
401417
},
402418
)
419+
@NvmeofCLICommand("nvmeof host add")
403420
@empty_response
404421
@handle_nvmeof_error
405422
def create(self, nqn: str, host_nqn: str, gw_group: Optional[str] = None):
@@ -415,6 +432,7 @@ def create(self, nqn: str, host_nqn: str, gw_group: Optional[str] = None):
415432
"gw_group": Param(str, "NVMeoF gateway group", True, None),
416433
},
417434
)
435+
@NvmeofCLICommand("nvmeof host del")
418436
@empty_response
419437
@handle_nvmeof_error
420438
def delete(self, nqn: str, host_nqn: str, gw_group: Optional[str] = None):
@@ -432,6 +450,7 @@ class NVMeoFConnection(RESTController):
432450
"gw_group": Param(str, "NVMeoF gateway group", True, None),
433451
},
434452
)
453+
@NvmeofCLICommand("nvmeof connection list")
435454
@map_collection(model.Connection, pick="connections")
436455
@handle_nvmeof_error
437456
def list(self, nqn: str, gw_group: Optional[str] = None):

src/pybind/mgr/dashboard/module.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
create_self_signed_cert, get_default_addr, verify_tls_files
3030

3131
from . import mgr
32+
from .controllers import nvmeof # noqa # pylint: disable=unused-import
3233
from .controllers import Router, json_error_page
3334
from .grafana import push_local_dashboards
3435
from .services import nvmeof_cli # noqa # pylint: disable=unused-import

src/pybind/mgr/dashboard/services/nvmeof_cli.py

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
11
# -*- coding: utf-8 -*-
22
import errno
33
import json
4+
from typing import Any, Dict, Optional
45

5-
from mgr_module import CLICheckNonemptyFileInput, CLIReadCommand, CLIWriteCommand
6+
import yaml
7+
from mgr_module import CLICheckNonemptyFileInput, CLICommand, CLIReadCommand, \
8+
CLIWriteCommand, HandleCommandResult, HandlerFuncType
69

10+
from ..exceptions import DashboardException
711
from ..rest_client import RequestException
812
from .nvmeof_conf import ManagedByOrchestratorException, \
913
NvmeofGatewayAlreadyExists, NvmeofGatewaysConfig
@@ -45,3 +49,39 @@ def remove_nvmeof_gateway(_, name: str, daemon_name: str = ''):
4549
return 0, 'Success', ''
4650
except ManagedByOrchestratorException as ex:
4751
return -errno.EINVAL, '', str(ex)
52+
53+
54+
class NvmeofCLICommand(CLICommand):
55+
def __call__(self, func) -> HandlerFuncType: # type: ignore
56+
# pylint: disable=useless-super-delegation
57+
"""
58+
This method is being overriden solely to be able to disable the linters checks for typing.
59+
The NvmeofCLICommand decorator assumes a different type returned from the
60+
function it wraps compared to CLICmmand, breaking a Liskov substitution principal,
61+
hence triggering linters alerts.
62+
"""
63+
return super().__call__(func)
64+
65+
def call(self,
66+
mgr: Any,
67+
cmd_dict: Dict[str, Any],
68+
inbuf: Optional[str] = None) -> HandleCommandResult:
69+
try:
70+
ret = super().call(mgr, cmd_dict, inbuf)
71+
out_format = cmd_dict.get('format')
72+
if out_format == 'json' or not out_format:
73+
if ret is None:
74+
out = ''
75+
else:
76+
out = json.dumps(ret)
77+
elif out_format == 'yaml':
78+
if ret is None:
79+
out = ''
80+
else:
81+
out = yaml.dump(ret)
82+
else:
83+
return HandleCommandResult(-errno.EINVAL, '',
84+
f"format '{out_format}' is not implemented")
85+
return HandleCommandResult(0, out, '')
86+
except DashboardException as e:
87+
return HandleCommandResult(-errno.EINVAL, '', str(e))

src/pybind/mgr/dashboard/services/nvmeof_client.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@
1313
import grpc._channel # type: ignore
1414
from google.protobuf.message import Message # type: ignore
1515

16-
from .proto import gateway_pb2 as pb2
17-
from .proto import gateway_pb2_grpc as pb2_grpc
16+
from .proto import gateway_pb2 as pb2 # type: ignore
17+
from .proto import gateway_pb2_grpc as pb2_grpc # type: ignore
1818
except ImportError:
1919
grpc = None
2020
else:
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import errno
2+
from unittest.mock import MagicMock
3+
4+
import pytest
5+
from mgr_module import CLICommand, HandleCommandResult
6+
7+
from ..services.nvmeof_cli import NvmeofCLICommand
8+
9+
10+
@pytest.fixture(scope="class", name="sample_command")
11+
def fixture_sample_command():
12+
test_cmd = "test command"
13+
14+
@NvmeofCLICommand(test_cmd)
15+
def func(_): # noqa # pylint: disable=unused-variable
16+
return {'a': '1', 'b': 2}
17+
yield test_cmd
18+
del NvmeofCLICommand.COMMANDS[test_cmd]
19+
assert test_cmd not in NvmeofCLICommand.COMMANDS
20+
21+
22+
@pytest.fixture(name='base_call_mock')
23+
def fixture_base_call_mock(monkeypatch):
24+
mock_result = {'a': 'b'}
25+
super_mock = MagicMock()
26+
super_mock.return_value = mock_result
27+
monkeypatch.setattr(CLICommand, 'call', super_mock)
28+
return super_mock
29+
30+
31+
@pytest.fixture(name='base_call_return_none_mock')
32+
def fixture_base_call_return_none_mock(monkeypatch):
33+
mock_result = None
34+
super_mock = MagicMock()
35+
super_mock.return_value = mock_result
36+
monkeypatch.setattr(CLICommand, 'call', super_mock)
37+
return super_mock
38+
39+
40+
class TestNvmeofCLICommand:
41+
def test_command_being_added(self, sample_command):
42+
assert sample_command in NvmeofCLICommand.COMMANDS
43+
assert isinstance(NvmeofCLICommand.COMMANDS[sample_command], NvmeofCLICommand)
44+
45+
def test_command_return_cmd_result_default_format(self, base_call_mock, sample_command):
46+
result = NvmeofCLICommand.COMMANDS[sample_command].call(MagicMock(), {})
47+
assert isinstance(result, HandleCommandResult)
48+
assert result.retval == 0
49+
assert result.stdout == '{"a": "b"}'
50+
assert result.stderr == ''
51+
base_call_mock.assert_called_once()
52+
53+
def test_command_return_cmd_result_json_format(self, base_call_mock, sample_command):
54+
result = NvmeofCLICommand.COMMANDS[sample_command].call(MagicMock(), {'format': 'json'})
55+
assert isinstance(result, HandleCommandResult)
56+
assert result.retval == 0
57+
assert result.stdout == '{"a": "b"}'
58+
assert result.stderr == ''
59+
base_call_mock.assert_called_once()
60+
61+
def test_command_return_cmd_result_yaml_format(self, base_call_mock, sample_command):
62+
result = NvmeofCLICommand.COMMANDS[sample_command].call(MagicMock(), {'format': 'yaml'})
63+
assert isinstance(result, HandleCommandResult)
64+
assert result.retval == 0
65+
assert result.stdout == 'a: b\n'
66+
assert result.stderr == ''
67+
base_call_mock.assert_called_once()
68+
69+
def test_command_return_cmd_result_invalid_format(self, base_call_mock, sample_command):
70+
mock_result = {'a': 'b'}
71+
super_mock = MagicMock()
72+
super_mock.call.return_value = mock_result
73+
74+
result = NvmeofCLICommand.COMMANDS[sample_command].call(MagicMock(), {'format': 'invalid'})
75+
assert isinstance(result, HandleCommandResult)
76+
assert result.retval == -errno.EINVAL
77+
assert result.stdout == ''
78+
assert result.stderr
79+
base_call_mock.assert_called_once()
80+
81+
def test_command_return_empty_cmd_result(self, base_call_return_none_mock, sample_command):
82+
result = NvmeofCLICommand.COMMANDS[sample_command].call(MagicMock(), {})
83+
assert isinstance(result, HandleCommandResult)
84+
assert result.retval == 0
85+
assert result.stdout == ''
86+
assert result.stderr == ''
87+
base_call_return_none_mock.assert_called_once()

0 commit comments

Comments
 (0)