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/__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 59efdb5..0b30fde 100644 --- a/nvmet/nvme.py +++ b/nvmet/nvme.py @@ -22,8 +22,9 @@ import stat import uuid import json +import subprocess +import shlex from glob import iglob as glob -from six import iteritems, moves DEFAULT_SAVE_FILE = '/etc/nvmet/config.json' @@ -220,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: @@ -234,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') @@ -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() @@ -462,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)] @@ -510,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) @@ -518,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 @@ -556,7 +582,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 @@ -567,7 +593,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) @@ -629,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): ''' @@ -816,7 +934,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/nvmetcli b/nvmetcli index d949891..385e257 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 @@ -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: @@ -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) @@ -166,6 +177,7 @@ class UISubsystemNode(UINode): self._children = set([]) UINamespacesNode(self) UIAllowedHostsNode(self) + UIPassthruNode(self) def summary(self): info = [] @@ -175,6 +187,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): @@ -283,6 +371,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) @@ -704,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) @@ -737,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)) 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