diff --git a/nvmet.json b/nvmet.json index 608fc5181efc6aadef9aed3a4e241c55c45c4c49..2064d4fc7c17e3d118cd0b9082a3f775e7f3aad9 100644 --- a/nvmet.json +++ b/nvmet.json @@ -1,4 +1,17 @@ { + "ports": [ + { + "addr": { + "adrfam": "ipv4", + "traddr": "192.168.7.68", + "treq": "not specified", + "trsvcid": "1023", + "trtype": "rdma" + }, + "enable": 1, + "portid": 2 + } + ], "subsystems": [ { "namespaces": [ diff --git a/nvmet/nvme.py b/nvmet/nvme.py index 8e5fb574cab989b1d482214b0c7ae94aba39aa29..0d6cdbd92da93c5517075a6fef539461f62d6a2f 100644 --- a/nvmet/nvme.py +++ b/nvmet/nvme.py @@ -263,6 +263,15 @@ class Root(CFSNode): subsystems = property(_list_subsystems, doc="Get the list of Subsystems.") + def _list_ports(self): + self._check_self() + + for d in os.listdir("%s/ports/" % self._path): + yield Port(self, d, 'lookup') + + ports = property(_list_ports, + doc="Get the list of Ports.") + def save_to_file(self, savefile=None): ''' Write the configuration in json format to a file. @@ -289,6 +298,8 @@ class Root(CFSNode): for s in self.subsystems: s.delete() + for p in self.ports: + p.delete() def restore(self, config, clear_existing=False, abort_on_error=False): ''' @@ -319,6 +330,13 @@ class Root(CFSNode): Subsystem.setup(t, err_func) + for index, t in enumerate(config.get('ports', [])): + if 'portid' not in t: + err_func("'portid' not defined in port %d" % index) + continue + + Port.setup(self, t, err_func) + return errors def restore_from_file(self, savefile=None, clear_existing=True, @@ -341,6 +359,7 @@ class Root(CFSNode): def dump(self): d = super(Root, self).dump() d['subsystems'] = [s.dump() for s in self.subsystems] + d['ports'] = [p.dump() for p in self.ports] return d @@ -517,6 +536,70 @@ class Namespace(CFSNode): return d +class Port(CFSNode): + ''' + This is an interface to a NVMe Namespace in configFS. + A Namespace is identified by its parent Subsystem and Namespace ID. + ''' + + MAX_PORTID = 8192 + + def __repr__(self): + return "<Port %d>" % self.portid + + def __init__(self, root, portid=None, mode='any'): + super(Port, self).__init__() + + if portid is None: + portids = [p.portid for p in root.ports] + for index in xrange(0, 1 << 16): + if index not in portids: + portid = index + break + if portid is None: + raise CFSError("All Port IDs 0-%d in use" % 1 << 16) + else: + portid = int(portid) + if portid < 0 or portid > self.MAX_PORTID: + raise CFSError("Port ID must be 0 to %d" % self.MAX_PORTID) + + self.attr_groups = ['addr'] + self._portid = portid + self._path = "%s/ports/%d" % (self.configfs_dir, portid) + self._create_in_cfs(mode) + + def _get_portid(self): + return self._portid + + portid = property(_get_portid, + doc="Get the Port ID as an int.") + + @classmethod + def setup(cls, root, n, err_func): + ''' + Set up a Namespace object based upon n dict, from saved config. + Guard against missing or bad dict items, but keep going. + Call 'err_func' for each error. + ''' + + if 'portid' not in n: + err_func("'portid' not defined for Port") + return + + try: + port = Port(root, n['portid']) + except CFSError as e: + err_func("Could not create Port object: %s" % e) + return + + port._setup_attrs(n, err_func) + + def dump(self): + d = super(Port, self).dump() + d['portid'] = self.portid + return d + + def _test(): from doctest import testmod testmod() diff --git a/nvmet/test_nvmet.py b/nvmet/test_nvmet.py index 5654c59d6b96829e08ec2b1d2fd7fb9fed7309e5..4047348dc93672efa009a649548dcdf02c154db2 100644 --- a/nvmet/test_nvmet.py +++ b/nvmet/test_nvmet.py @@ -148,6 +148,103 @@ class TestNvmet(unittest.TestCase): s.delete() self.assertEqual(len(list(root.subsystems)), 0) + def test_port(self): + root = nvme.Root() + root.clear_existing() + for p in root.ports: + self.assertTrue(False, 'Found Port after clear') + + # create mode + p1 = nvme.Port(root, portid=0, mode='create') + self.assertIsNotNone(p1) + self.assertEqual(len(list(root.ports)), 1) + + # any mode, should create + p2 = nvme.Port(root, portid=1, mode='any') + self.assertIsNotNone(p2) + self.assertEqual(len(list(root.ports)), 2) + + # automatic portid + p3 = nvme.Port(root, mode='create') + self.assertIsNotNone(p3) + self.assertNotEqual(p3, p1) + self.assertNotEqual(p3, p2) + self.assertEqual(len(list(root.ports)), 3) + + # duplicate + self.assertRaises(nvme.CFSError, nvme.Port, + root, portid=0, mode='create') + self.assertEqual(len(list(root.ports)), 3) + + # lookup using any, should not create + p = nvme.Port(root, portid=0, mode='any') + self.assertEqual(p1, p) + self.assertEqual(len(list(root.ports)), 3) + + # lookup only + p = nvme.Port(root, portid=1, mode='lookup') + self.assertEqual(p2, p) + self.assertEqual(len(list(root.ports)), 3) + + # lookup without portid + self.assertRaises(nvme.CFSError, nvme.Port, root, mode='lookup') + + # and delete them all + for p in root.ports: + p.delete() + self.assertEqual(len(list(root.ports)), 0) + + def test_loop_port(self): + root = nvme.Root() + root.clear_existing() + + p = nvme.Port(root, portid=0, mode='create') + + self.assertFalse(p.get_enable()) + self.assertTrue('addr' in p.attr_groups) + + # no trtype set yet, should fail + self.assertRaises(nvme.CFSError, p.set_enable, 1) + + # now set trtype to loop and other attrs and enable + p.set_attr('addr', 'trtype', 'loop') + p.set_attr('addr', 'adrfam', 'ipv4') + p.set_attr('addr', 'traddr', '192.168.0.1') + p.set_attr('addr', 'treq', 'not required') + p.set_attr('addr', 'trsvcid', '1023') + p.set_enable(1) + self.assertTrue(p.get_enable()) + + # test double enable + p.set_enable(1) + + # test that we can't write to attrs while enabled + self.assertRaises(nvme.CFSError, p.set_attr, 'addr', 'trtype', + 'rdma') + self.assertRaises(nvme.CFSError, p.set_attr, 'addr', 'adrfam', + 'ipv6') + self.assertRaises(nvme.CFSError, p.set_attr, 'addr', 'traddr', + '10.0.0.1') + self.assertRaises(nvme.CFSError, p.set_attr, 'addr', 'treq', + 'required') + self.assertRaises(nvme.CFSError, p.set_attr, 'addr', 'trsvcid', + '21') + + # disable: once and twice + p.set_enable(0) + p.set_enable(0) + + # check that the attrs haven't been tampered with + self.assertEqual(p.get_attr('addr', 'trtype'), 'loop') + self.assertEqual(p.get_attr('addr', 'adrfam'), 'ipv4') + self.assertEqual(p.get_attr('addr', 'traddr'), '192.168.0.1') + self.assertEqual(p.get_attr('addr', 'treq'), 'not required') + self.assertEqual(p.get_attr('addr', 'trsvcid'), '1023') + + # enable again, and remove while enabled + p.set_enable(1) + p.delete() + def test_invalid_input(self): root = nvme.Root() root.clear_existing() @@ -167,6 +264,9 @@ class TestNvmet(unittest.TestCase): self.assertRaises(nvme.CFSError, nvme.Subsystem, nqn=discover_nqn, mode='create') + self.assertRaises(nvme.CFSError, nvme.Port, + root=root, portid=1 << 17, mode='create') + def test_save_restore(self): root = nvme.Root() root.clear_existing() @@ -179,6 +279,15 @@ class TestNvmet(unittest.TestCase): nguid = n.get_attr('device', 'nguid') + p = nvme.Port(root, portid=66, mode='create') + p.set_attr('addr', 'trtype', 'loop') + p.set_attr('addr', 'adrfam', 'ipv4') + p.set_attr('addr', 'traddr', '192.168.0.1') + p.set_attr('addr', 'treq', 'not required') + p.set_attr('addr', 'trsvcid', '1023') + p.set_enable(1) + + # save, clear, and restore root.save_to_file('test.json') root.clear_existing() root.restore_from_file('test.json') @@ -193,8 +302,15 @@ class TestNvmet(unittest.TestCase): # rebuild our view of the world s = nvme.Subsystem(nqn='testnqn', mode='lookup') n = nvme.Namespace(s, nsid=42, mode='lookup') + p = nvme.Port(root, portid=66, mode='lookup') # and check everything is still the same self.assertTrue(n.get_enable()) self.assertEqual(n.get_attr('device', 'path'), '/dev/ram0') self.assertEqual(n.get_attr('device', 'nguid'), nguid) + + self.assertEqual(p.get_attr('addr', 'trtype'), 'loop') + self.assertEqual(p.get_attr('addr', 'adrfam'), 'ipv4') + self.assertEqual(p.get_attr('addr', 'traddr'), '192.168.0.1') + self.assertEqual(p.get_attr('addr', 'treq'), 'not required') + self.assertEqual(p.get_attr('addr', 'trsvcid'), '1023') diff --git a/nvmetcli b/nvmetcli index 8573eb8555f202b00e98cc4e1df70e731562e02e..421970430c3e324c29d7e2645d4a9c63665feccb 100755 --- a/nvmetcli +++ b/nvmetcli @@ -94,6 +94,7 @@ class UIRootNode(UINode): def refresh(self): self._children = set([]) UISubsystemsNode(self) + UIPortsNode(self) def ui_command_restoreconfig(self, savefile=None, clear_existing=False): ''' @@ -235,7 +236,88 @@ class UINamespaceNode(UINode): self.shell.log.info("The Namespace has been disabled.") except Exception as e: raise configshell.ExecutionError( - "The Namespace could not be dsiabled.") + "The Namespace could not be disabled.") + + +class UIPortsNode(UINode): + def __init__(self, parent): + UINode.__init__(self, 'ports', parent) + + def refresh(self): + self._children = set([]) + for port in self.parent.cfnode.ports: + UIPortNode(self, port) + + def ui_command_create(self, portid): + ''' + Creates a new NVMe port. If I{port} is ommited, then the new Port + will use the next available portid. + + SEE ALSO + ======== + B{delete} + ''' + port = nvme.Port(self.parent.parent.cfnode, portid, mode='create') + UIPortNode(self, port) + + def ui_command_delete(self, portid): + ''' + Recursively deletes the NVMe Port with the specified I{port}, and all + objects hanging under it. + + SEE ALSO + ======== + B{delete} + ''' + port = nvme.Port(self.parent.parent.cfnode, portid, mode='lookup') + port.delete() + self.refresh() + + +class UIPortNode(UINode): + def __init__(self, parent, cfnode): + UINode.__init__(self, str(cfnode.portid), parent, cfnode) + + def status(self): + if self.cfnode.get_enable(): + return "enabled" + return "disabled" + + def ui_command_enable(self): + ''' + Enables the current Port. + + SEE ALSO + ======== + B{disable} + ''' + if self.cfnode.get_enable(): + self.shell.log.info("The Port is already enabled.") + else: + try: + self.cfnode.set_enable(1) + self.shell.log.info("The Port has been enabled.") + except Exception as e: + raise configshell.ExecutionError( + "The Port could not be enabled.") + + def ui_command_disable(self): + ''' + Disables the current Port. + + SEE ALSO + ======== + B{enable} + ''' + if not self.cfnode.get_enable(): + self.shell.log.info("The Port is already disabled.") + else: + try: + self.cfnode.set_enable(0) + self.shell.log.info("The Port has been disabled.") + except Exception as e: + raise configshell.ExecutionError( + "The Port could not be disabled.") def usage():