Skip to content
Snippets Groups Projects
Commit 86b1bca7 authored by Christoph Hellwig's avatar Christoph Hellwig
Browse files

nvmet,nvmetcli: add support for host NQN based access control


Signed-off-by: default avatarChristoph Hellwig <hch@lst.de>
parent fbf48aed
No related branches found
No related tags found
No related merge requests found
......@@ -38,24 +38,38 @@ arguments. Then in the nvmetcli prompt type:
#
> 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'.
......@@ -272,6 +272,15 @@ class Root(CFSNode):
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.
......@@ -300,6 +309,8 @@ class Root(CFSNode):
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):
'''
......@@ -323,6 +334,14 @@ class Root(CFSNode):
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)
......@@ -360,6 +379,7 @@ class Root(CFSNode):
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
......@@ -393,6 +413,7 @@ class Subsystem(CFSNode):
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)
......@@ -415,11 +436,39 @@ class Subsystem(CFSNode):
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):
'''
......@@ -440,11 +489,16 @@ class Subsystem(CFSNode):
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
......@@ -600,6 +654,57 @@ class Port(CFSNode):
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()
......
......@@ -245,6 +245,67 @@ class TestNvmet(unittest.TestCase):
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()
......@@ -271,7 +332,13 @@ class TestNvmet(unittest.TestCase):
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')
......@@ -300,10 +367,16 @@ class TestNvmet(unittest.TestCase):
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')
......
......@@ -95,6 +95,7 @@ class UIRootNode(UINode):
self._children = set([])
UISubsystemsNode(self)
UIPortsNode(self)
UIHostsNode(self)
def ui_command_restoreconfig(self, savefile=None, clear_existing=False):
'''
......@@ -151,6 +152,7 @@ class UISubsystemNode(UINode):
def refresh(self):
self._children = set([])
UINamespacesNode(self)
UIAllowedHostsNode(self)
class UINamespacesNode(UINode):
......@@ -239,6 +241,66 @@ class UINamespaceNode(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)
......@@ -320,6 +382,45 @@ class UIPortNode(UINode):
"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])
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment