#
> cd /subsystems
-/subsystems> create testnqn
+...> create testnqn
+
+#
+# Add access for a specific NVMe Host by it's NQN:
+#
+...> cd /hosts
+...> create hostnqn
+...> cd /subsystems/testnqn/allowed_hosts/
+...> create hostnqn
+
+#
+# Alternatively this allows any host to connect to the subsystsem. Only
+# use this in tightly controller environments:
+#
+...> cd /subsystems/testnqn/
+...> set attr allow_any_host=1
#
# Create a new namespace. If you do not specify a namespace ID the fist
# unused one will be used.
#
-/subsystems> cd testnqn/
-/subsystems/testnqn> cd namespaces
-/subsystems/testnqn/namespaces> create 1
-/subsystems/testnqn/namespaces> cd 1
-/subsystems/t.../namespaces/1> set device path=/dev/ram1
-/subsystems/t.../namespaces/1> enable
+...> cd namespaces
+...> create 1
+...> cd 1
+...> set device path=/dev/ram1
+...> enable
Testing
-------
-nvmetcli comes with a testsuite that tests itsels and the kernel configfs
+nvmetcli comes with a testsuite that tests itself and the kernel configfs
interface for the NVMe target. To run it make sure you have nose2 and
the coverage plugin for it installed and simple run 'make test'.
ports = property(_list_ports,
doc="Get the list of Ports.")
+ def _list_hosts(self):
+ self._check_self()
+
+ for h in os.listdir("%s/hosts/" % self._path):
+ yield Host(h, 'lookup')
+
+ hosts = property(_list_hosts,
+ doc="Get the list of Hosts.")
+
def save_to_file(self, savefile=None):
'''
Write the configuration in json format to a file.
s.delete()
for p in self.ports:
p.delete()
+ for h in self.hosts:
+ h.delete()
def restore(self, config, clear_existing=False, abort_on_error=False):
'''
def err_func(err_str):
errors.append(err_str + ", skipped")
+ # Create the hosts first because the subsystems reference them
+ for index, t in enumerate(config.get('hosts', [])):
+ if 'nqn' not in t:
+ err_func("'nqn' not defined in host %d" % index)
+ continue
+
+ Host.setup(t, err_func)
+
for index, t in enumerate(config.get('subsystems', [])):
if 'nqn' not in t:
err_func("'nqn' not defined in subsystem %d" % index)
d = super(Root, self).dump()
d['subsystems'] = [s.dump() for s in self.subsystems]
d['ports'] = [p.dump() for p in self.ports]
+ d['hosts'] = [h.dump() for h in self.hosts]
return d
nqn = self._generate_nqn()
self.nqn = nqn
+ self.attr_groups = ['attr']
self._path = "%s/subsystems/%s" % (self.configfs_dir, nqn)
self._create_in_cfs(mode)
self._check_self()
for ns in self.namespaces:
ns.delete()
+ for h in self.allowed_hosts:
+ self.remove_allowed_host(h)
super(Subsystem, self).delete()
namespaces = property(_list_namespaces,
doc="Get the list of Namespaces for the Subsystem.")
+ def _list_allowed_hosts(self):
+ return [os.path.basename(name)
+ for name in os.listdir("%s/allowed_hosts/" % self._path)]
+
+ allowed_hosts = property(_list_allowed_hosts,
+ doc="Get the list of Allowed Hosts for the Subsystem.")
+
+ def add_allowed_host(self, nqn):
+ '''
+ Enable access for the host identified by I{nqn} to the Subsystem
+ '''
+ try:
+ os.symlink("%s/hosts/%s" % (self.configfs_dir, nqn),
+ "%s/allowed_hosts/%s" % (self._path, nqn))
+ except Exception as e:
+ raise CFSError("Could not symlink %s in configFS: %s" % (nqn, e))
+
+ def remove_allowed_host(self, nqn):
+ '''
+ Disable access for the host identified by I{nqn} to the Subsystem
+ '''
+ try:
+ os.unlink("%s/allowed_hosts/%s" % (self._path, nqn))
+ except Exception as e:
+ raise CFSError("Could not unlink %s in configFS: %s" % (nqn, e))
+
@classmethod
def setup(cls, t, err_func):
'''
for ns in t.get('namespaces', []):
Namespace.setup(s, ns, err_func)
+ for h in t.get('allowed_hosts', []):
+ s.add_allowed_host(h)
+
+ s._setup_attrs(t, err_func)
def dump(self):
d = super(Subsystem, self).dump()
d['nqn'] = self.nqn
d['namespaces'] = [ns.dump() for ns in self.namespaces]
+ d['allowed_hosts'] = self.allowed_hosts
return d
return d
+class Host(CFSNode):
+ '''
+ This is an interface to a NVMe Host in configFS.
+ A Host is identified by its NQN.
+ '''
+
+ def __repr__(self):
+ return "<Host %s>" % self.nqn
+
+ def __init__(self, nqn, mode='any'):
+ '''
+ @param nqn: The Hosts's NQN.
+ @type nqn: string
+ @param mode:An optional string containing the object creation mode:
+ - I{'any'} means the configFS object will be either looked up
+ or created.
+ - I{'lookup'} means the object MUST already exist configFS.
+ - I{'create'} means the object must NOT already exist in configFS.
+ @type mode:string
+ @return: A Host object.
+ '''
+ super(Host, self).__init__()
+
+ self.nqn = nqn
+ self._path = "%s/hosts/%s" % (self.configfs_dir, nqn)
+ self._create_in_cfs(mode)
+
+ @classmethod
+ def setup(cls, t, err_func):
+ '''
+ Set up Host objects based upon t dict, from saved config.
+ Guard against missing or bad dict items, but keep going.
+ Call 'err_func' for each error.
+ '''
+
+ if 'nqn' not in t:
+ err_func("'nqn' not defined for Host")
+ return
+
+ try:
+ h = Host(t['nqn'])
+ except CFSError as e:
+ err_func("Could not create Host object: %s" % e)
+ return
+
+ def dump(self):
+ d = super(Host, self).dump()
+ d['nqn'] = self.nqn
+ return d
+
+
def _test():
from doctest import testmod
testmod()
p.set_enable(1)
p.delete()
+ def test_host(self):
+ root = nvme.Root()
+ root.clear_existing()
+ for p in root.hosts:
+ self.assertTrue(False, 'Found Host after clear')
+
+ # create mode
+ h1 = nvme.Host(nqn='foo', mode='create')
+ self.assertIsNotNone(h1)
+ self.assertEqual(len(list(root.hosts)), 1)
+
+ # any mode, should create
+ h2 = nvme.Host(nqn='bar', mode='any')
+ self.assertIsNotNone(h2)
+ self.assertEqual(len(list(root.hosts)), 2)
+
+ # duplicate
+ self.assertRaises(nvme.CFSError, nvme.Host,
+ 'foo', mode='create')
+ self.assertEqual(len(list(root.hosts)), 2)
+
+ # lookup using any, should not create
+ h = nvme.Host('foo', mode='any')
+ self.assertEqual(h1, h)
+ self.assertEqual(len(list(root.hosts)), 2)
+
+ # lookup only
+ h = nvme.Host('bar', mode='lookup')
+ self.assertEqual(h2, h)
+ self.assertEqual(len(list(root.hosts)), 2)
+
+ # and delete them all
+ for h in root.hosts:
+ h.delete()
+ self.assertEqual(len(list(root.hosts)), 0)
+
+ def test_allowed_hosts(self):
+ root = nvme.Root()
+
+ h = nvme.Host(nqn='hostnqn', mode='create')
+
+ s = nvme.Subsystem(nqn='testnqn', mode='create')
+
+ # add allowed_host
+ s.add_allowed_host(nqn='hostnqn')
+
+ # duplicate
+ self.assertRaises(nvme.CFSError, s.add_allowed_host, 'hostnqn')
+
+ # invalid
+ self.assertRaises(nvme.CFSError, s.add_allowed_host, 'invalid')
+
+ # remove again
+ s.remove_allowed_host('hostnqn')
+
+ # duplicate removal
+ self.assertRaises(nvme.CFSError, s.remove_allowed_host, 'hostnqn')
+
+ # invalid removal
+ self.assertRaises(nvme.CFSError, s.remove_allowed_host, 'foobar')
+
def test_invalid_input(self):
root = nvme.Root()
root.clear_existing()
root = nvme.Root()
root.clear_existing()
+ h = nvme.Host(nqn='hostnqn', mode='create')
+
s = nvme.Subsystem(nqn='testnqn', mode='create')
+ s.add_allowed_host(nqn='hostnqn')
+
+ s2 = nvme.Subsystem(nqn='testnqn2', mode='create')
+ s2.set_attr('attr', 'allow_any_host', 1)
n = nvme.Namespace(s, nsid=42, mode='create')
n.set_attr('device', 'path', '/dev/ram0')
root.restore_from_file('test.json', True)
# rebuild our view of the world
+ h = nvme.Host(nqn='hostnqn', mode='lookup')
s = nvme.Subsystem(nqn='testnqn', mode='lookup')
+ s2 = nvme.Subsystem(nqn='testnqn2', mode='lookup')
n = nvme.Namespace(s, nsid=42, mode='lookup')
p = nvme.Port(root, portid=66, mode='lookup')
+ self.assertEqual(s.get_attr('attr', 'allow_any_host'), "0")
+ self.assertEqual(s2.get_attr('attr', 'allow_any_host'), "1")
+ self.assertIn('hostnqn', s.allowed_hosts)
+
# and check everything is still the same
self.assertTrue(n.get_enable())
self.assertEqual(n.get_attr('device', 'path'), '/dev/ram0')
self._children = set([])
UISubsystemsNode(self)
UIPortsNode(self)
+ UIHostsNode(self)
def ui_command_restoreconfig(self, savefile=None, clear_existing=False):
'''
def refresh(self):
self._children = set([])
UINamespacesNode(self)
+ UIAllowedHostsNode(self)
class UINamespacesNode(UINode):
"The Namespace could not be disabled.")
+class UIAllowedHostsNode(UINode):
+ def __init__(self, parent):
+ UINode.__init__(self, 'allowed_hosts', parent)
+
+ def refresh(self):
+ self._children = set([])
+ for host in self.parent.cfnode.allowed_hosts:
+ UIAllowedHostNode(self, host)
+
+ def ui_command_create(self, nqn):
+ '''
+ Grants access to parent subsystems to the host specified by I{nqn}.
+
+ SEE ALSO
+ ========
+ B{delete}
+ '''
+ self.parent.cfnode.add_allowed_host(nqn)
+ UIAllowedHostNode(self, nqn)
+
+ def ui_complete_create(self, parameters, text, current_param):
+ completions = []
+ if current_param == 'nqn':
+ for host in self.get_node('/hosts').children:
+ completions.append(host.cfnode.nqn)
+
+ if len(completions) == 1:
+ return [completions[0] + ' ']
+ else:
+ return completions
+
+ def ui_command_delete(self, nqn):
+ '''
+ Recursively deletes the namespace with the specified I{nsid}, and all
+ objects hanging under it.
+
+ SEE ALSO
+ ========
+ B{create}
+ '''
+ self.parent.cfnode.remove_allowed_host(nqn)
+ self.refresh()
+
+ def ui_complete_delete(self, parameters, text, current_param):
+ completions = []
+ if current_param == 'nqn':
+ for nqn in self.parent.cfnode.allowed_hosts:
+ completions.append(nqn)
+
+ if len(completions) == 1:
+ return [completions[0] + ' ']
+ else:
+ return completions
+
+
+class UIAllowedHostNode(UINode):
+ def __init__(self, parent, nqn):
+ UINode.__init__(self, nqn, parent)
+
+
class UIPortsNode(UINode):
def __init__(self, parent):
UINode.__init__(self, 'ports', parent)
"The Port could not be disabled.")
+class UIHostsNode(UINode):
+ def __init__(self, parent):
+ UINode.__init__(self, 'hosts', parent)
+
+ def refresh(self):
+ self._children = set([])
+ for host in self.parent.cfnode.hosts:
+ UIHostNode(self, host)
+
+ def ui_command_create(self, nqn):
+ '''
+ Creates a new NVMe host.
+
+ SEE ALSO
+ ========
+ B{delete}
+ '''
+ host = nvme.Host(nqn, mode='create')
+ UIHostNode(self, host)
+
+ def ui_command_delete(self, nqn):
+ '''
+ Recursively deletes the NVMe Host with the specified I{nqn}, and all
+ objects hanging under it.
+
+ SEE ALSO
+ ========
+ B{create}
+ '''
+ host = nvme.Host(nqn, mode='lookup')
+ host.delete()
+ self.refresh()
+
+
+class UIHostNode(UINode):
+ def __init__(self, parent, cfnode):
+ UINode.__init__(self, cfnode.nqn, parent, cfnode)
+
+
def usage():
print("syntax: %s save [file_to_save_to]" % sys.argv[0])
print(" %s restore [file_to_restore_from]" % sys.argv[0])