]> www.infradead.org Git - users/hch/nvmetcli.git/commitdiff
nvmet,nvmetcli: add support for host NQN based access control
authorChristoph Hellwig <hch@lst.de>
Sun, 3 Apr 2016 15:44:35 +0000 (17:44 +0200)
committerChristoph Hellwig <hch@lst.de>
Sun, 3 Apr 2016 16:33:37 +0000 (18:33 +0200)
Signed-off-by: Christoph Hellwig <hch@lst.de>
README.md
nvmet/nvme.py
nvmet/test_nvmet.py
nvmetcli

index 3ece072cd89a256a3c050cbf7a10d6e1258f135a..75ca072ed82cb12d3c6e22a058ad9542cfd1345a 100644 (file)
--- a/README.md
+++ b/README.md
@@ -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'.
index 7629107d476c8f8811e68e559609343a0fd7b07d..d991d9ba90b253fc20a5de8ce35a6bdd577bad69 100644 (file)
@@ -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()
index 4047348dc93672efa009a649548dcdf02c154db2..d2dbb4a19aa46d492ac5ed0699eff62e3bf1f00b 100644 (file)
@@ -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')
index 91d01cafb6e946b591394a32cc0aebdf6a920605..c2ec343e8d75e7649420d37788b3d3f5a7c2320d 100755 (executable)
--- a/nvmetcli
+++ b/nvmetcli
@@ -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])