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():