From 844f6c2a0bfaec22c37d6ab2069c6b49e0efc032 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 Ales: handle if resv_enable doesn't exist --- 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 d269988d4dc2f098b666905905eff4816cd03dd1 Mon Sep 17 00:00:00 2001 From: Michal Suchanek Date: Fri, 21 Mar 2025 11:59:10 +0100 Subject: [PATCH 2/7] 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. Ales: use subprocess.run() instead of system(), take into account if there are extra arguments in /proc/sys/kernel/modprobe. --- 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 2dfb7a2881823fb228b7117c01f0820dacd9cbbd 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 Ales: handle if kernel doesn't support passthru target. Fix typos (Namespace->Passthru, tomeout->timeout) --- nvmet/__init__.py | 2 +- nvmet/nvme.py | 103 ++++++++++++++++++++++++++++++++++++++++++++++ nvmetcli | 77 ++++++++++++++++++++++++++++++++++ 3 files changed, 181 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..89c94eb 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)] @@ -525,6 +532,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 +542,8 @@ def dump(self): d['nqn'] = self.nqn d['namespaces'] = [ns.dump() for ns in self.namespaces] d['allowed_hosts'] = self.allowed_hosts + if os.path.isdir(os.path.join(self.path, "passthru")): + d['passthru'] = [self.passthru.dump()] return d @@ -644,6 +655,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..8f22dfb 100755 --- a/nvmetcli +++ b/nvmetcli @@ -166,6 +166,7 @@ class UISubsystemNode(UINode): self._children = set([]) UINamespacesNode(self) UIAllowedHostsNode(self) + UIPassthruNode(self) def summary(self): info = [] @@ -175,6 +176,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): + pass + + 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 abfe575e8cdeff5125afefb11e503f948955d187 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] chore: remove use of the Python six package. 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. Ales: s/xrange/range/ --- 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 89c94eb..84758df 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: @@ -582,7 +581,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 @@ -934,7 +933,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 d8c509897a2448d6762e3e13fc898b2efcef1528 Mon Sep 17 00:00:00 2001 From: Hannes Reinecke Date: Fri, 22 Aug 2025 13:54:49 +0200 Subject: [PATCH 5/7] '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 Ales: handle lack of support of discovery_nqn --- nvmet/nvme.py | 1 + nvmetcli | 11 +++++++++++ 2 files changed, 12 insertions(+) diff --git a/nvmet/nvme.py b/nvmet/nvme.py index 84758df..0b30fde 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 8f22dfb..65f6c88 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 cb1df04090a9467150f1ec9f0b9df14580e85c70 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] 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 --- nvmetcli | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nvmetcli b/nvmetcli index 65f6c88..e15c865 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 c7cb5c692831da2f0ea56c1507bdc35b5e84aeee 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] 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 --- nvmetcli | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/nvmetcli b/nvmetcli index e15c865..385e257 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: @@ -797,7 +797,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) @@ -830,7 +830,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))