From e9e01e7b1a93b8a04bb2c6af30f321f8ea48e3a2 Mon Sep 17 00:00:00 2001 From: Guixin Liu Date: Tue, 8 Oct 2024 10:08:00 +0200 Subject: [PATCH 1/7] nvmetcli: reservation configuration support Add support for reservation configuration. Signed-off-by: Guixin Liu [alnovak: handle if resv_enable doesn't exist] Signed-off-by: Ales Novak Signed-off-by: Daniel Wagner --- nvmet/nvme.py | 2 +- nvmetcli | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/nvmet/nvme.py b/nvmet/nvme.py index 59efdb5..20b3cca 100644 --- a/nvmet/nvme.py +++ b/nvmet/nvme.py @@ -567,7 +567,7 @@ def __init__(self, subsystem, nsid=None, mode='any'): if nsid < 1 or nsid > self.MAX_NSID: raise CFSError("NSID must be 1 to %d" % self.MAX_NSID) - self.attr_groups = ['device', 'ana'] + self.attr_groups = ['device', 'ana', 'resv'] self._subsystem = subsystem self._nsid = nsid self._path = "%s/namespaces/%d" % (self.subsystem.path, self.nsid) diff --git a/nvmetcli b/nvmetcli index d949891..1ff115c 100755 --- a/nvmetcli +++ b/nvmetcli @@ -283,6 +283,11 @@ class UINamespaceNode(UINode): info.append("nguid=" + ns_nguid) if self.cfnode.grpid != 0: info.append("grpid=" + str(self.cfnode.grpid)) + try: + resv_enable = self.cfnode.get_attr("resv", "enable") + info.append("resv_enable=" + str(resv_enable)) + except nvme.nvme.CFSError: + pass info.append("enabled" if self.cfnode.get_enable() else "disabled") ns_enabled = self.cfnode.get_enable() return (", ".join(info), True if ns_enabled == 1 else ns_enabled) From 029304ac5d2f01cf1cc1430b56208f53fdf23e91 Mon Sep 17 00:00:00 2001 From: Michal Suchanek Date: Fri, 21 Mar 2025 11:59:10 +0100 Subject: [PATCH 2/7] nvmetcli: when kmodpy is not available call kmod binary directly kmodpy is an unmaintained project. Python is not really good at backwards compatibility. With the upstream project not keeping up with python churn using the library is a maintenance burden. nvmet does not use the python library in any substantial way, it only loads a module once. This can be easily accomplished without any library using the modprobe tool directly. Signed-off-by: Michal Suchanek [alnovak: use subprocess.run() instead of system(), take into account if there are extra arguments in /proc/sys/kernel/modprobe.] Signed-off-by: Ales Novak Signed-off-by: Daniel Wagner --- nvmet/nvme.py | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/nvmet/nvme.py b/nvmet/nvme.py index 20b3cca..da0b664 100644 --- a/nvmet/nvme.py +++ b/nvmet/nvme.py @@ -22,6 +22,8 @@ import stat import uuid import json +import subprocess +import shlex from glob import iglob as glob from six import iteritems, moves @@ -256,9 +258,22 @@ def _modprobe(self, modname): # Try the ctypes library included with the libkmod itself. try: import kmod - kmod.Kmod().modprobe(modname) - except Exception as e: - pass + + try: + kmod.Kmod().modprobe(modname) + except Exception as e: + pass + except ImportError: + # Try the binary specified in /proc + try: + modprobe_cmd = None + with open('/proc/sys/kernel/modprobe', 'r') as f: + modprobe_cmd = f.read() + if modprobe_cmd: + subprocess.run(shlex.split(modprobe_cmd) + [modname], + check=False) + except Exception as e: + pass def _list_subsystems(self): self._check_self() From a9434fbbe459ed68da0475da4027a62be2a427f7 Mon Sep 17 00:00:00 2001 From: Nilay Shroff Date: Wed, 11 Jun 2025 12:39:29 +0200 Subject: [PATCH 3/7] nvmetcli: add support for NVMe passthru target This change adds the support for configuring NVMe passthru target. Signed-off-by: Nilay Shroff [alnovak: - handle lack of passthru support in the kernel - align refresh() method with others - fix typos (Namespace->Passthru, tomeout->timeout) - passhtru: fix when passhtru doesn't exist] Signed-off-by: Ales Novak Signed-off-by: Daniel Wagner --- nvmet/__init__.py | 2 +- nvmet/nvme.py | 106 ++++++++++++++++++++++++++++++++++++++++++++++ nvmetcli | 78 ++++++++++++++++++++++++++++++++++ 3 files changed, 185 insertions(+), 1 deletion(-) diff --git a/nvmet/__init__.py b/nvmet/__init__.py index cf172bd..5e2f525 100644 --- a/nvmet/__init__.py +++ b/nvmet/__init__.py @@ -1,2 +1,2 @@ -from .nvme import Root, Subsystem, Namespace, Port, Host, Referral, ANAGroup,\ +from .nvme import Root, Subsystem, Namespace, Port, Host, Referral, ANAGroup, Passthru, \ DEFAULT_SAVE_FILE diff --git a/nvmet/nvme.py b/nvmet/nvme.py index da0b664..ebb01b1 100644 --- a/nvmet/nvme.py +++ b/nvmet/nvme.py @@ -477,6 +477,13 @@ def _list_namespaces(self): namespaces = property(_list_namespaces, doc="Get the list of Namespaces for the Subsystem.") + def _get_passthru(self): + self._check_self() + return Passthru(self) + + passthru = property(_get_passthru, + doc="Get the passthru node for the subsystem") + def _list_allowed_hosts(self): return [os.path.basename(name) for name in os.listdir("%s/allowed_hosts/" % self._path)] @@ -503,6 +510,9 @@ def remove_allowed_host(self, nqn): except Exception as e: raise CFSError("Could not unlink %s in configFS: %s" % (nqn, e)) + def has_passthru(self): + return os.path.isdir(os.path.join(self.path, "passthru")) + @classmethod def setup(cls, t, err_func): ''' @@ -525,6 +535,8 @@ def setup(cls, t, err_func): Namespace.setup(s, ns, err_func) for h in t.get('allowed_hosts', []): s.add_allowed_host(h) + for pt in t.get('passthru', []): + Passthru.setup(s, pt, err_func) s._setup_attrs(t, err_func) @@ -533,6 +545,8 @@ def dump(self): d['nqn'] = self.nqn d['namespaces'] = [ns.dump() for ns in self.namespaces] d['allowed_hosts'] = self.allowed_hosts + if self.has_passthru(): + d['passthru'] = [self.passthru.dump()] return d @@ -644,6 +658,98 @@ def dump(self): d['ana_grpid'] = self.grpid return d +class Passthru(CFSNode): + ''' + This is an interface to a NVMe passthru in ConfigFS. + ''' + + def __init__(self, subsystem): + ''' + @param subsystem: The parent Subsystem object. + @return: A Passthru object. + ''' + super(Passthru, self).__init__() + self._path = "%s/passthru" % (subsystem.path) + self.attr_groups = ['device'] + + def _get_clear_ids(self): + self._check_self() + path = "%s/clear_ids" % self.path + _ids = 0 + if os.path.isfile(path): + with open(path, 'r') as file_fd: + _ids = int(file_fd.read().strip()) + return _ids + + ids = property(_get_clear_ids, + doc = "Get the passthru namespace clear_ids attribute.") + + def set_clear_ids(self, clear): + self._check_self() + path = "%s/clear_ids" % self.path + if os.path.isfile(path): + with open(path, 'w') as file_fd: + file_fd.write(str(clear)) + + def _get_admin_timeout(self): + self._check_self() + path = "%s/admin_timeout" % self.path + _timeout = 0 + if os.path.isfile(path): + with open(path, 'r') as file_fd: + _timeout = int(file_fd.read().strip()) + return _timeout + + admin_timeout = property(_get_admin_timeout, + doc = "Get the passthru admin command timeout.") + + def set_admin_timeout(self, timeout): + self._check_self() + path = "%s/admin_timeout" % self.path + if os.path.isfile(path): + with open(path, 'w') as file_fd: + file_fd.write(str(timeout)) + + def _get_io_timeout(self): + self._check_self() + path = "%s/io_timeout" % self.path + _timeout = 0 + if os.path.isfile(path): + with open(path, 'r') as file_fd: + _timeout = int(file_fd.read().strip()) + return _timeout + + io_timeout = property(_get_io_timeout, + doc = "Get the passthru IO command timeout.") + + def set_io_timeout(self, timeout): + self._check_self() + path = "%s/io_timeout" % self.path + if os.path.isfile(path): + with open(path, 'w') as file_fd: + file_fd.write(str(timeout)) + + @classmethod + def setup(cls, subsys, p, err_func): + try: + pt = Passthru(subsys) + except CFSError as e: + err_func("Could not create Passthru object: %s" % e) + return + pt._setup_attrs(p, err_func) + if 'clear_ids' in p: + pt.set_clear_ids(int(p['clear_ids'])) + if 'admin_timeout' in p: + pt.set_admin_timeout(int(p['admin_timeout'])) + if 'io_timeout' in p: + pt.set_io_timeout(int(p['io_timeout'])) + + def dump(self): + d = super(Passthru, self).dump() + d['clear_ids'] = self.ids + d['admin_timeout'] = self.admin_timeout + d['io_timeout'] = self.io_timeout + return d class Port(CFSNode): ''' diff --git a/nvmetcli b/nvmetcli index 1ff115c..a2aa64b 100755 --- a/nvmetcli +++ b/nvmetcli @@ -166,6 +166,8 @@ class UISubsystemNode(UINode): self._children = set([]) UINamespacesNode(self) UIAllowedHostsNode(self) + if self.cfnode.has_passthru(): + UIPassthruNode(self) def summary(self): info = [] @@ -175,6 +177,82 @@ class UISubsystemNode(UINode): info.append("serial=" + self.cfnode.get_attr("attr", "serial")) return (", ".join(info), True) +class UIPassthruNode(UINode): + ui_desc_device = { + 'path' : ('string', 'Passthru device path') + } + + def __init__(self, parent): + passthru = nvme.Passthru(parent.cfnode) + UINode.__init__(self, 'passthru', parent, passthru) + + def refresh(self): + self._children = set([]) + + def ui_command_enable(self): + if self.cfnode.get_enable(): + self.shell.log.info("The passthru is already enabled.") + else: + try: + self.cfnode.set_enable(1) + self.shell.log.info("The passthru has been enabled.") + except Exception as e: + raise configshell.ExecutionError( + "The passthru could not be enabled.") + + def ui_command_disable(self): + if not self.cfnode.get_enable(): + self.shell.log.info("The passthru is already disabled.") + else: + try: + self.cfnode.set_enable(0) + self.shell.log.info("The passthru has been disabled.") + except Exception as e: + raise configshell.ExecutionError( + "The passthru could not be disabled.") + + def ui_command_clear_ids(self, clear): + ''' + If I{clear} is set to non-zero then clears the passthru namespace + unique identifiers EUI/GUID/UUID. + ''' + try: + self.cfnode.set_clear_ids(clear) + except Exception as e: + raise configshell.ExecutionError( + "Failed to set clear_ids for this passthru target.") + + def ui_command_admin_timeout(self, timeout): + ''' + Sets the timeout of admin passthru command. + ''' + try: + self.cfnode.set_admin_timeout(timeout) + except Exception as e: + raise configshell.ExecutionError( + "Failed to set the admin passthru command timeout.") + + def ui_command_io_timeout(self, timeout): + ''' + Sets the timeout of IO passthru command. + ''' + try: + self.cfnode.set_io_timeout(timeout) + except Exception as e: + raise configshell.ExecutionError( + "Failed to set the IO passthru command timeout.") + + def summary(self): + info = [] + info.append("path=" + self.cfnode.get_attr("device", "path")) + if self.cfnode.ids != 0: + info.append("clear_ids=" + str(self.cfnode.ids)) + if self.cfnode.admin_timeout != 0: + info.append("admin_timeout=" + str(self.cfnode.admin_timeout)) + if self.cfnode.io_timeout != 0: + info.append("io_timeout=" + str(self.cfnode.io_timeout)) + info.append("enabled" if self.cfnode.get_enable() else "disabled") + return (", ".join(info), True) class UINamespacesNode(UINode): def __init__(self, parent): From 112cd9df2aa0a120831889b22956b882c6f596f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mat=C4=9Bj=20Cepl?= Date: Wed, 18 Jun 2025 17:08:11 +0200 Subject: [PATCH 4/7] nvmetcli: remove use of the Python six package. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Python 2 has been truly and completely dead for over five years, I don't know about any Linux distribution which wouldn't have Python 3 as its default Python. Please, let it die. Signed-off-by: Matěj Cepl [alnovak: s/xrange/range/] Signed-off-by: Ales Novak Signed-off-by: Daniel Wagner --- README | 3 +-- nvmet/nvme.py | 7 +++---- rpm/nvmetcli.spec.tmpl | 2 +- 3 files changed, 5 insertions(+), 7 deletions(-) diff --git a/README b/README index 6ebe666..6a7beb1 100644 --- a/README +++ b/README @@ -14,8 +14,7 @@ using setup.py. Common Package Dependencies and Problems ----------------------------------------- -Both python2 and python3 are supported via use of the 'python-six' -package. +Python3 is supported. nvmetcli uses the 'pyparsing' package -- running nvmetcli without this package may produce hard-to-decipher errors. diff --git a/nvmet/nvme.py b/nvmet/nvme.py index ebb01b1..c6a8304 100644 --- a/nvmet/nvme.py +++ b/nvmet/nvme.py @@ -25,7 +25,6 @@ import subprocess import shlex from glob import iglob as glob -from six import iteritems, moves DEFAULT_SAVE_FILE = '/etc/nvmet/config.json' @@ -222,7 +221,7 @@ def dump(self): def _setup_attrs(self, attr_dict, err_func): for group in self.attr_groups: - for name, value in iteritems(attr_dict.get(group, {})): + for name, value in attr_dict.get(group, {}).items(): try: self.set_attr(group, name, value) except CFSError as e: @@ -585,7 +584,7 @@ def __init__(self, subsystem, nsid=None, mode='any'): raise CFSError("Need NSID for lookup") nsids = [n.nsid for n in subsystem.namespaces] - for index in moves.xrange(1, self.MAX_NSID + 1): + for index in range(1, self.MAX_NSID + 1): if index not in nsids: nsid = index break @@ -937,7 +936,7 @@ def __init__(self, port, grpid, mode='any'): raise CFSError("Need grpid for lookup") grpids = [n.grpid for n in port.ana_groups] - for index in moves.xrange(2, self.MAX_GRPID + 1): + for index in range(2, self.MAX_GRPID + 1): if index not in grpids: grpid = index break diff --git a/rpm/nvmetcli.spec.tmpl b/rpm/nvmetcli.spec.tmpl index f1b5533..ce454a2 100644 --- a/rpm/nvmetcli.spec.tmpl +++ b/rpm/nvmetcli.spec.tmpl @@ -9,7 +9,7 @@ Source: nvmetcli-%{version}.tar.gz BuildRoot: %{_tmppath}/%{name}-%{version}-%{release}-rpmroot BuildArch: noarch BuildRequires: python-devel python-setuptools systemd-units -Requires: python-configshell python-kmod python-six +Requires: python-configshell python-kmod Requires(post): systemd Requires(preun): systemd Requires(postun): systemd From cdf9b0de8b2e725f9cee979436603fb975ed4b3c Mon Sep 17 00:00:00 2001 From: Hannes Reinecke Date: Fri, 22 Aug 2025 13:54:49 +0200 Subject: [PATCH 5/7] nvmetcli: add discovery_nqn configfs attribute Implement support for the 'discovery_nqn' configfs attribute to allow the user to read or modify the persistent discovery NQN. Signed-off-by: Hannes Reinecke [alnovak: handle lack of support of discovery_nqn] Signed-off-by: Ales Novak Signed-off-by: Daniel Wagner --- nvmet/nvme.py | 1 + nvmetcli | 11 +++++++++++ 2 files changed, 12 insertions(+) diff --git a/nvmet/nvme.py b/nvmet/nvme.py index c6a8304..faf1eb2 100644 --- a/nvmet/nvme.py +++ b/nvmet/nvme.py @@ -235,6 +235,7 @@ class Root(CFSNode): def __init__(self): super(Root, self).__init__() + self.attr_groups = ['discovery'] if not os.path.isdir(self.configfs_dir): self._modprobe('nvmet') diff --git a/nvmetcli b/nvmetcli index a2aa64b..b29de95 100755 --- a/nvmetcli +++ b/nvmetcli @@ -94,10 +94,21 @@ class UINode(configshell.node.ConfigNode): class UIRootNode(UINode): + ui_desc_discovery = { + 'nqn': ('string', 'Discovery NQN'), + } def __init__(self, shell): UINode.__init__(self, '/', parent=None, cfnode=nvme.Root(), shell=shell) + def summary(self): + info = [] + try: + info.append("discovery=" + self.cfnode.get_attr("discovery", "nqn")) + except nvme.nvme.CFSError: + pass + return (", ".join(info), True) + def refresh(self): self._children = set([]) UISubsystemsNode(self) From a69adaa44466a2637d6757641975d65221b40f73 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stephan=20M=C3=BCller?= Date: Thu, 9 Oct 2025 18:45:03 +0200 Subject: [PATCH 6/7] nvmetcli: use preferred module name MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The configshell-fb module is now called configshell. From v2.0.0 onwards the preferred way is to import the module as configshell. Signed-off-by: Stephan Müller Signed-off-by: Daniel Wagner --- nvmetcli | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nvmetcli b/nvmetcli index b29de95..7286653 100755 --- a/nvmetcli +++ b/nvmetcli @@ -22,7 +22,7 @@ from __future__ import print_function import os import sys -import configshell_fb as configshell +import configshell import nvmet as nvme import errno from string import hexdigits From d320306cde0ae081a4b7ea207ceb256c096bcea8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stephan=20M=C3=BCller?= Date: Thu, 9 Oct 2025 18:45:04 +0200 Subject: [PATCH 7/7] nvmetcli: use exported configshell symbols MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace direct invocations of configshell internals with actually exported symbols. Signed-off-by: Stephan Müller Signed-off-by: Daniel Wagner --- nvmetcli | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/nvmetcli b/nvmetcli index 7286653..859d22b 100755 --- a/nvmetcli +++ b/nvmetcli @@ -33,9 +33,9 @@ def ngiud_set(nguid): return any(c in hexdigits and c != '0' for c in nguid) -class UINode(configshell.node.ConfigNode): +class UINode(configshell.ConfigNode): def __init__(self, name, parent=None, cfnode=None, shell=None): - configshell.node.ConfigNode.__init__(self, name, parent, shell) + configshell.ConfigNode.__init__(self, name, parent, shell) self.cfnode = cfnode if self.cfnode: if self.cfnode.attr_groups: @@ -798,7 +798,7 @@ def clear(unused): def ls(unused): - shell = configshell.shell.ConfigShell('~/.nvmetcli') + shell = configshell.ConfigShell('~/.nvmetcli') UIRootNode(shell) shell.run_cmdline("ls") sys.exit(0) @@ -831,7 +831,7 @@ def main(): return try: - shell = configshell.shell.ConfigShell('~/.nvmetcli') + shell = configshell.ConfigShell('~/.nvmetcli') UIRootNode(shell) except Exception as msg: shell.log.error(str(msg))