--- /dev/null
+*.swp
+*.swo
+build-stamp
+build/*
+dist/*
+doc/*
+*.pyc
+*.pyc
--- /dev/null
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
--- /dev/null
+PKGNAME = nvmetcli
+NAME = nvmet
+GIT_BRANCH = $$(git branch | grep \* | tr -d \*)
+VERSION = $$(basename $$(git describe --tags | tr - . | sed 's/^v//'))
+
+all:
+ @echo "Usage:"
+ @echo
+ @echo " make release - Generates the release tarball."
+ @echo
+ @echo " make clean - Cleanup the local repository build files."
+ @echo " make cleanall - Also remove dist/*"
+
+clean:
+ @rm -fv ${NAME}/*.pyc ${NAME}/*.html
+ @rm -frv doc
+ @rm -frv ${NAME}.egg-info MANIFEST build
+ @rm -fv build-stamp
+ @rm -frv results
+ @rm -frv ${PKGNAME}-*
+ @echo "Finished cleanup."
+
+cleanall: clean
+ @rm -frv dist
+
+release: build/release-stamp
+build/release-stamp:
+ @mkdir -p build
+ @echo "Exporting the repository files..."
+ @git archive ${GIT_BRANCH} --prefix ${PKGNAME}-${VERSION}/ \
+ | (cd build; tar xfp -)
+ @echo "Cleaning up the target tree..."
+ @rm -f build/${PKGNAME}-${VERSION}/Makefile
+ @rm -f build/${PKGNAME}-${VERSION}/.gitignore
+ @echo "Fixing version string..."
+ @sed -i "s/__version__ = .*/__version__ = '${VERSION}'/g" \
+ build/${PKGNAME}-${VERSION}/${NAME}/__init__.py
+ @find build/${PKGNAME}-${VERSION}/ -exec \
+ touch -t $$(date -d @$$(git show -s --format="format:%at") \
+ +"%Y%m%d%H%M.%S") {} \;
+ @mkdir -p dist
+ @cd build; tar -c --owner=0 --group=0 --numeric-owner \
+ --format=gnu -b20 --quoting-style=escape \
+ -f ../dist/${PKGNAME}-${VERSION}.tar \
+ $$(find ${PKGNAME}-${VERSION} -type f | sort)
+ @gzip -6 -n dist/${PKGNAME}-${VERSION}.tar
+ @echo "Generated release tarball:"
+ @echo " $$(ls dist/${PKGNAME}-${VERSION}.tar.gz)"
+ @touch build/release-stamp
--- /dev/null
+
+nvmetcli
+========
+
+This contains the NVMe target admin tool "nvmetcli". It can either be
+used interactively by invoking it without arguments, or it can be used
+to save, restore or clear the current NVMe target configuration.
+
+
+Installation
+------------
+
+Please install the configshell-fb package from
+https://github.com/agrover/configshell-fb first.
+
+Nvmetcli can be run directly from the source directory or installed
+using setup.py.
+
+
+Usage
+-----
+
+Make sure to run nvmetcli as root, the nvmet module is loaded and
+configfs is mounted on /sys/kernel/config, using:
+
+ mount -t configs none /sys/kernel/config
+
+You can load the default config that exports the first NVMe device and
+the first ramdisk by running "nvmetcli restore nvmet.json". The default
+config is stored in /etc/nvmet.json. You can also edit the json file
+directly.
+
+To get started with the interactive mode start nvmetcli without
+arguments. Then in the nvmetcli prompt type:
+
+#
+# Create a subsystem. If you do not specify a name a NQN will be generated.
+#
+
+> cd /subsystems
+/subsystems> create testnqn
+
+#
+# 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
--- /dev/null
+{
+ "subsystems": [
+ {
+ "namespaces": [
+ {
+ "device": {
+ "path": "/dev/ram0"
+ },
+ "nsid": 2
+ },
+ {
+ "device": {
+ "path": "/dev/nvme0n1"
+ },
+ "nsid": 1
+ }
+ ],
+ "nqn": "nqn.2014-08.org.nvmexpress:NVMf:uuid:77dca664-0d3e-4f67-b8b2-04c70e3f991d"
+ }
+ ]
+}
--- /dev/null
+from nvme import Root, Subsystem, Namespace
--- /dev/null
+'''
+Implements access to the NVMe target configfs hierarchy
+
+Copyright (c) 2011-2013 by Datera, Inc.
+Copyright (c) 2011-2014 by Red Hat, Inc.
+Copyright (c) 2016 by HGST, a Western Digital Company.
+
+Licensed under the Apache License, Version 2.0 (the "License"); you may
+not use this file except in compliance with the License. You may obtain
+a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+License for the specific language governing permissions and limitations
+under the License.
+'''
+
+import os
+import stat
+import uuid
+import json
+from glob import iglob as glob
+
+DEFAULT_SAVE_FILE = 'saveconfig.json'
+
+
+class CFSError(Exception):
+ '''
+ Generic slib error.
+ '''
+ pass
+
+
+class CFSNotFound(CFSError):
+ '''
+ The underlying configfs object does not exist. Happens when
+ calling methods of an object that is instantiated but have
+ been deleted from congifs, or when trying to lookup an
+ object that does not exist.
+ '''
+ pass
+
+
+class CFSNode(object):
+
+ configfs_dir = '/sys/kernel/config/nvmet'
+
+ def __init__(self):
+ self._path = self.configfs_dir
+ self._attr_groups = []
+
+ def __eq__(self, other):
+ return self._path == other._path
+
+ def __ne__(self, other):
+ return self._path != other._path
+
+ def _get_path(self):
+ return self._path
+
+ def _create_in_cfs(self, mode):
+ '''
+ Creates the configFS node if it does not already exist, depending on
+ the mode.
+ any -> makes sure it exists, also works if the node already does exist
+ lookup -> make sure it does NOT exist
+ create -> create the node which must not exist beforehand
+ '''
+ if mode not in ['any', 'lookup', 'create']:
+ raise CFSError("Invalid mode: %s" % mode)
+ if self.exists and mode == 'create':
+ raise CFSError("This %s already exists in configFS"
+ % self.__class__.__name__)
+ elif not self.exists and mode == 'lookup':
+ raise CFSNotFound("No such %s in configfs: %s"
+ % (self.__class__.__name__, self.path))
+
+ if not self.exists:
+ try:
+ os.mkdir(self.path)
+ except:
+ raise CFSError("Could not create %s in configFS"
+ % self.__class__.__name__)
+
+ def _exists(self):
+ return os.path.isdir(self.path)
+
+ def _check_self(self):
+ if not self.exists:
+ raise CFSNotFound("This %s does not exist in configFS"
+ % self.__class__.__name__)
+
+ def list_attrs(self, group, writable=None):
+ '''
+ @param group: The attribute group
+ @param writable: If None (default), returns all attributes, if True,
+ returns read-write attributes, if False, returns just the read-only
+ attributes.
+ @type writable: bool or None
+ @return: A list of existing attribute names as strings.
+ '''
+ self._check_self()
+
+ names = [os.path.basename(name).split('_')[1]
+ for name in glob("%s/%s_*" % (self._path, group))
+ if os.path.isfile(name)]
+
+ if writable is True:
+ names = [name for name in names
+ if self._attr_is_writable(group, name)]
+ elif writable is False:
+ names = [name for name in names
+ if not self._attr_is_writable(group, name)]
+
+ names.sort()
+ return names
+
+ def _attr_is_writable(self, group, name):
+ s = os.stat("%s/%s_%s" % (self._path, group, name))
+ return s[stat.ST_MODE] & stat.S_IWUSR
+
+ def set_attr(self, group, attribute, value):
+ '''
+ Sets the value of a named attribute.
+ The attribute must exist in configFS.
+ @param group: The attribute group
+ @param attribute: The attribute's name.
+ @param value: The attribute's value.
+ @type value: string
+ '''
+ self._check_self()
+ path = "%s/%s_%s" % (self.path, str(group), str(attribute))
+
+ if not os.path.isfile(path):
+ raise CFSError("Cannot find attribute: %s" % path)
+
+ try:
+ with open(path, 'w') as file_fd:
+ file_fd.write(str(value))
+ except Exception as e:
+ raise CFSError("Cannot set attribute %s: %s" % (path, e))
+
+ def get_attr(self, group, attribute):
+ '''
+ Gets the value of a named attribute.
+ @param group: The attribute group
+ @param attribute: The attribute's name.
+ @return: The named attribute's value, as a string.
+ '''
+ self._check_self()
+ path = "%s/%s_%s" % (self.path, str(group), str(attribute))
+ if not os.path.isfile(path):
+ raise CFSError("Cannot find attribute: %s" % path)
+
+ with open(path, 'r') as file_fd:
+ return file_fd.read().strip()
+
+ def delete(self):
+ '''
+ If the underlying configFS object does not exist, this method does
+ nothing. If the underlying configFS object exists, this method attempts
+ to delete it.
+ '''
+ if self.exists:
+ os.rmdir(self.path)
+
+ path = property(_get_path,
+ doc="Get the configFS object path.")
+ exists = property(_exists,
+ doc="Is True as long as the underlying configFS object exists. "
+ + "If the underlying configFS objects gets deleted "
+ + "either by calling the delete() method, or by any "
+ + "other means, it will be False.")
+
+ def dump(self):
+ d = {}
+ for group in self._attr_groups:
+ a = {}
+ for i in self.list_attrs(group, writable=True):
+ a[str(i)] = self.get_attr(group, i)
+ d[str(group)] = a
+ return d
+
+ def _setup_attrs(self, attr_dict, err_func):
+ for group in self._attr_groups:
+ for name, value in attr_dict.get(group, {}).iteritems():
+ try:
+ self.set_attr(group, name, value)
+ except CFSError as e:
+ err_func(str(e))
+
+
+class Root(CFSNode):
+ def __init__(self):
+ super(Root, self).__init__()
+ self._path = self.configfs_dir
+ self._create_in_cfs('lookup')
+
+ def _list_subsystems(self):
+ self._check_self()
+
+ for d in os.listdir("%s/subsystems/" % self._path):
+ yield Subsystem(d, 'lookup')
+
+ subsystems = property(_list_subsystems,
+ doc="Get the list of Subsystems.")
+
+ def save_to_file(self, savefile=None):
+ '''
+ Write the configuration in json format to a file.
+ Save file defaults to '/etc/targets/saveconfig.json'.
+ '''
+ if savefile:
+ savefile = os.path.expanduser(savefile)
+ else:
+ savefile = DEFAULT_SAVE_FILE
+
+ with open(savefile + ".temp", "w+") as f:
+ os.fchmod(f.fileno(), stat.S_IRUSR | stat.S_IWUSR)
+ f.write(json.dumps(self.dump(), sort_keys=True, indent=2))
+ f.write("\n")
+ os.fsync(f.fileno())
+
+ os.rename(savefile + ".temp", savefile)
+
+ def clear_existing(self):
+ '''
+ Remove entire current configuration.
+ '''
+
+ for s in self.subsystems:
+ s.delete()
+
+ def restore(self, config, clear_existing=False, abort_on_error=False):
+ '''
+ Takes a dict generated by dump() and reconfigures the target to match.
+ Returns list of non-fatal errors that were encountered.
+ Will refuse to restore over an existing configuration unless
+ clear_existing is True.
+ '''
+ if clear_existing:
+ self.clear_existing()
+ else:
+ if any(self.subsystems):
+ raise CFSError("subsystems present, not restoring")
+
+ errors = []
+
+ if abort_on_error:
+ def err_func(err_str):
+ raise CFSError(err_str)
+ else:
+ def err_func(err_str):
+ errors.append(err_str + ", skipped")
+
+ for index, t in enumerate(config.get('subsystems', [])):
+ if 'nqn' not in t:
+ err_func("'nqn' not defined in subsystem %d" % index)
+ continue
+
+ Subsystem.setup(t, err_func)
+
+ return errors
+
+ def restore_from_file(self, savefile=None, clear_existing=True,
+ abort_on_error=False):
+ '''
+ Restore the configuration from a file in json format.
+ Returns a list of non-fatal errors. If abort_on_error is set,
+ it will raise the exception instead of continuing.
+ '''
+ if savefile:
+ savefile = os.path.expanduser(savefile)
+ else:
+ savefile = DEFAULT_SAVE_FILE
+
+ with open(savefile, "r") as f:
+ config = json.loads(f.read())
+ return self.restore(config, clear_existing=clear_existing,
+ abort_on_error=abort_on_error)
+
+ def dump(self):
+ d = super(Root, self).dump()
+ d['subsystems'] = [s.dump() for s in self.subsystems]
+ return d
+
+
+class Subsystem(CFSNode):
+ '''
+ This is an interface to a NVMe Subsystem in configFS.
+ A Subsystem is identified by its NQN.
+ '''
+
+ def __repr__(self):
+ return "<Namespace %s>" % self.nqn
+
+ def __init__(self, nqn=None, mode='any'):
+ '''
+ @param nqn: The optional Target's NQN.
+ If no NQN is specified, one will be generated.
+ @type nqn: string
+ @param mode:An optionnal 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 Subsystem object.
+ '''
+ super(Subsystem, self).__init__()
+
+ if nqn is None:
+ nqn = self._generate_nqn()
+
+ self.nqn = nqn
+ self._path = "%s/subsystems/%s" % (self.configfs_dir, nqn)
+ self._create_in_cfs(mode)
+
+ def _generate_nqn(self):
+ prefix = "nqn.2014-08.org.nvmexpress:NVMf:uuid"
+ name = str(uuid.uuid4())
+ return "%s:%s" % (prefix, name)
+
+ def _list_namespaces(self):
+ self._check_self()
+ for d in os.listdir("%s/namespaces/" % self._path):
+ yield Namespace(self, int(d), 'lookup')
+
+ def delete(self):
+ '''
+ Recursively deletes a Subsystems object.
+ This will delete all attached Namespace objects and then the
+ Subsystem itself.
+ '''
+ self._check_self()
+ for ns in self.namespaces:
+ ns.delete()
+ super(Subsystem, self).delete()
+
+ namespaces = property(_list_namespaces,
+ doc="Get the list of Namespaces for the Subsystem.")
+
+ @classmethod
+ def setup(cls, t, err_func):
+ '''
+ Set up Subsystems 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 Subsystem")
+ return
+
+ try:
+ s = Subsystem(t['nqn'])
+ except CFSError as e:
+ err_func("Could not create Subsystem object: %s" % e)
+ return
+
+ for ns in t.get('namespaces', []):
+ Namespace.setup(s, ns, err_func)
+
+ def dump(self):
+ d = super(Subsystem, self).dump()
+ d['nqn'] = self.nqn
+ d['namespaces'] = [ns.dump() for ns in self.namespaces]
+ return d
+
+
+class Namespace(CFSNode):
+ '''
+ This is an interface to a NVMe Namespace in configFS.
+ A Namespace is identified by its parent Subsystem and Namespace ID.
+ '''
+
+ MAX_NSID = 8192
+
+ def __repr__(self):
+ return "<Namspace %d>" % self.nsid
+
+ def __init__(self, subsystem, nsid=None, mode='any'):
+ '''
+ A LUN object can be instanciated in two ways:
+ - B{Creation mode}: If I{storage_object} is specified, the
+ underlying configFS object will be created with that parameter.
+ No LUN with the same I{lun} index can pre-exist in the parent TPG
+ in that mode, or instanciation will fail.
+ - B{Lookup mode}: If I{storage_object} is not set, then the LUN
+ will be bound to the existing configFS LUN object of the parent
+ TPG having the specified I{lun} index. The underlying configFS
+ object must already exist in that mode.
+
+ @param parent_tpg: The parent TPG object.
+ @type parent_tpg: TPG
+ @param lun: The LUN index.
+ @type lun: 0-255
+ @param storage_object: The storage object to be exported as a LUN.
+ @type storage_object: StorageObject subclass
+ @param alias: An optional parameter to manually specify the LUN alias.
+ You probably do not need this.
+ @type alias: string
+ @return: A LUN object.
+ '''
+ super(Namespace, self).__init__()
+
+ if not isinstance(subsystem, Subsystem):
+ raise CFSError("Invalid parent class")
+
+ if nsid is None:
+ nsids = [n.nsid for n in self.subsystem.namespaces]
+ for index in xrange(1, self.MAX_NSID + 1):
+ if index not in nsids:
+ nsid = index
+ break
+ if nsid is None:
+ raise CFSError("All NSIDs 0-%d in use" % self.MAX_NSID)
+ else:
+ nsid = int(nsid)
+ if nsid < 0 or nsid > self.MAX_NSID:
+ raise CFSError("NSID must be 0 to %d" % self.MAX_NSID)
+
+ self._attr_groups = ['device']
+ self._subsystem = subsystem
+ self._nsid = nsid
+ self._path = "%s/namespaces/%d" % (self.subsystem.path, self.nsid)
+ self._create_in_cfs(mode)
+
+ def _get_subsystem(self):
+ return self._subsystem
+
+ def _get_nsid(self):
+ return self._nsid
+
+ subsystem = property(_get_subsystem,
+ doc="Get the parent Subsystem object.")
+ nsid = property(_get_nsid,
+ doc="Get the NSID as an int.")
+
+ @classmethod
+ def setup(cls, subsys, 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 'nsid' not in n:
+ err_func("'nsid' not defined for Namespace")
+ return
+
+ try:
+ ns = Namespace(subsys, n['nsid'])
+ except CFSError as e:
+ err_func("Could not create Namespace object: %s" % e)
+ return
+
+ ns._setup_attrs(n, err_func)
+
+ def dump(self):
+ d = super(Namespace, self).dump()
+ d['nsid'] = self.nsid
+ return d
+
+
+def _test():
+ from doctest import testmod
+ testmod()
+
+if __name__ == "__main__":
+ _test()
--- /dev/null
+#!/usr/bin/python
+
+'''
+Frontend to access to the NVMe target configfs hierarchy
+
+Copyright (c) 2016 by HGST, a Western Digital Company.
+
+Licensed under the Apache License, Version 2.0 (the "License"); you may
+not use this file except in compliance with the License. You may obtain
+a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+License for the specific language governing permissions and limitations
+under the License.
+'''
+
+import sys
+import configshell_fb as configshell
+import nvmet.nvme as nvme
+
+DEFAULT_SAVE_FILE = "/etc/nvmet.json"
+
+
+class UIRootNode(configshell.node.ConfigNode):
+ def __init__(self, shell):
+ configshell.node.ConfigNode.__init__(self, '/', shell=shell)
+ self.cfnode = nvme.Root()
+ self.refresh()
+
+ def refresh(self):
+ self._children = set([])
+ UISubsystemsNode(self)
+
+ def ui_command_saveconfig(self, savefile=None):
+ '''
+ Saves the current configuration to a file so that it can be restored
+ on next boot.
+ '''
+ self.cfnode.save_to_file(savefile)
+
+ def ui_command_restoreconfig(self, savefile=None, clear_existing=False):
+ '''
+ Restores configuration from a file.
+ '''
+ errors = self.cfnode.restore_from_file(savefile, clear_existing)
+ self.refresh()
+
+ if errors:
+ raise configshell.ExecutionError(
+ "Configuration restored, %d errors:\n%s" %
+ (len(errors), "\n".join(errors)))
+
+
+class UISubsystemsNode(configshell.node.ConfigNode):
+ def __init__(self, parent):
+ configshell.node.ConfigNode.__init__(self, 'subsystems', parent)
+ self._parent = parent
+ self.refresh()
+
+ def refresh(self):
+ self._children = set([])
+ for subsys in self._parent.cfnode.subsystems:
+ UISubsystemNode(self, subsys)
+
+ def ui_command_create(self, nqn=None):
+ '''
+ Creates a new target. If I{nqn} is ommited, then the new Subsystem
+ will be created using a randomly generated NQN.
+
+ SEE ALSO
+ ========
+ B{delete}
+ '''
+ subsystem = nvme.Subsystem(nqn, mode='create')
+ UISubsystemNode(self, subsystem)
+
+ def ui_command_delete(self, nqn):
+ '''
+ Recursively deletes the subsystem with the specified I{nqn}, and all
+ objects hanging under it.
+
+ SEE ALSO
+ ========
+ B{delete}
+ '''
+ subsystem = nvme.Subsystem(nqn, mode='lookup')
+ subsystem.delete()
+ self.refresh()
+
+
+class UISubsystemNode(configshell.node.ConfigNode):
+ def __init__(self, parent, cfnode):
+ configshell.node.ConfigNode.__init__(self, cfnode.nqn, parent)
+ self.cfnode = cfnode
+ self.refresh()
+
+ def refresh(self):
+ self._children = set([])
+ UINamespacesNode(self)
+
+
+class UINamespacesNode(configshell.node.ConfigNode):
+ def __init__(self, parent):
+ configshell.node.ConfigNode.__init__(self, 'namespaces', parent)
+ self._parent = parent
+ self.refresh()
+
+ def refresh(self):
+ self._children = set([])
+ for ns in self._parent.cfnode.namespaces:
+ UINamespaceNode(self, ns)
+
+ def ui_command_create(self, nsid=None):
+ '''
+ Creates a new namespace. If I{nsid} is ommited, then the next
+ available namespace id will be used.
+
+ SEE ALSO
+ ========
+ B{delete}
+ '''
+ namespace = nvme.Namespace(self._parent.cfnode, nsid, mode='create')
+ UINamespaceNode(self, namespace)
+
+ def ui_command_delete(self, nsid):
+ '''
+ Recursively deletes the namespace with the specified I{nsid}, and all
+ objects hanging under it.
+
+ SEE ALSO
+ ========
+ B{delete}
+ '''
+ namespace = nvme.Namespace(self._parent.cfnode, nsid, mode='lookup')
+ namespace.delete()
+ self.refresh()
+
+
+class UINamespaceNode(configshell.node.ConfigNode):
+ def __init__(self, parent, cfnode):
+ configshell.node.ConfigNode.__init__(self, str(cfnode.nsid), parent)
+ self.cfnode = cfnode
+ self.refresh()
+
+ def refresh(self):
+ self._children = set([])
+ self._init_group('device')
+
+ def _init_group(self, group):
+ attrs = self.cfnode.list_attrs(group)
+ attrs_ro = self.cfnode.list_attrs(group, writable=False)
+ for attr in attrs:
+ writable = attr not in attrs_ro
+ name = "ui_desc_%s" % group
+
+ t, d = getattr(self.__class__, name, {}).get(attr, ('string', ''))
+ self.define_config_group_param(group, attr, t, d, writable)
+
+ def ui_getgroup_device(self, attr):
+ return self.cfnode.get_attr('device', attr)
+
+ def ui_setgroup_device(self, attr, value):
+ return self.cfnode.set_attr('device', attr, value)
+
+
+class UIControllersNode(configshell.node.ConfigNode):
+ def __init__(self, parent):
+ configshell.node.ConfigNode.__init__(self, 'controllers', parent)
+ self._controllers = [0, 1]
+ self.refresh()
+
+ def refresh(self):
+ self._children = set([])
+ for ctrl in self._controllers:
+ UIControllerNode(self, ctrl)
+
+
+class UIControllerNode(configshell.node.ConfigNode):
+ def __init__(self, parent, cntlid):
+ configshell.node.ConfigNode.__init__(self, str(cntlid), parent)
+
+
+def usage():
+ print("syntax: %s save [file_to_save_to]" % sys.argv[0])
+ print(" %s restore [file_to_restore_from]" % sys.argv[0])
+ print(" %s clear" % sys.argv[0])
+ sys.exit(-1)
+
+
+def save(to_file):
+ nvme.Root().save_to_file(to_file)
+
+
+def restore(from_file):
+ try:
+ errors = nvme.Root().restore_from_file(from_file)
+ except IOError:
+ # Not an error if the restore file is not present
+ print("No saved config file at %s, ok, exiting" % from_file)
+ sys.exit(0)
+
+ for error in errors:
+ print(error)
+
+
+def clear(unused):
+ nvme.Root().clear_existing()
+
+
+funcs = dict(save=save, restore=restore, clear=clear)
+
+
+def main():
+ if len(sys.argv) > 3:
+ usage()
+
+ if len(sys.argv) == 2 or len(sys.argv) == 3:
+ if sys.argv[1] == "--help":
+ usage()
+
+ if sys.argv[1] not in funcs.keys():
+ usage()
+
+ if len(sys.argv) == 3:
+ savefile = sys.argv[2]
+ else:
+ savefile = None
+
+ funcs[sys.argv[1]](savefile)
+ return
+
+ shell = configshell.shell.ConfigShell('~/.nvmetcli')
+ UIRootNode(shell)
+
+ while not shell._exit:
+ try:
+ shell.run_interactive()
+ except configshell.ExecutionError as msg:
+ shell.log.error(str(msg))
+
+if __name__ == "__main__":
+ main()
--- /dev/null
+#! /usr/bin/env python
+'''
+This file is part of ConfigShell.
+Copyright (c) 2011-2013 by Datera, Inc
+
+Licensed under the Apache License, Version 2.0 (the "License"); you may
+not use this file except in compliance with the License. You may obtain
+a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+License for the specific language governing permissions and limitations
+under the License.
+'''
+
+from setuptools import setup
+
+setup(
+ name = 'nvmetcli',
+ version = '0.1',
+ description = 'NVMe target configuration tool',
+ license = 'Apache 2.0',
+ maintainer = 'Christoph Hellwig',
+ maintainer_email = 'hch@lst.de',
+ packages = ['nvmet'],
+ )