From: Christoph Hellwig Date: Wed, 16 Mar 2016 16:44:56 +0000 (+0100) Subject: initial public release X-Git-Tag: v0.1 X-Git-Url: https://www.infradead.org/git/?a=commitdiff_plain;h=72b210d0ce48434a57df171e256af7840b5111ff;p=users%2Fhch%2Fnvmetcli.git initial public release --- 72b210d0ce48434a57df171e256af7840b5111ff diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..be778db --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +*.swp +*.swo +build-stamp +build/* +dist/* +doc/* +*.pyc +*.pyc diff --git a/COPYING b/COPYING new file mode 100644 index 0000000..68c771a --- /dev/null +++ b/COPYING @@ -0,0 +1,176 @@ + + 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. + diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..e1887a6 --- /dev/null +++ b/Makefile @@ -0,0 +1,49 @@ +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 diff --git a/README.md b/README.md new file mode 100644 index 0000000..7a8a56b --- /dev/null +++ b/README.md @@ -0,0 +1,52 @@ + +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 diff --git a/nvmet.json b/nvmet.json new file mode 100644 index 0000000..8b5309c --- /dev/null +++ b/nvmet.json @@ -0,0 +1,21 @@ +{ + "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" + } + ] +} diff --git a/nvmet/__init__.py b/nvmet/__init__.py new file mode 100644 index 0000000..eba0ebb --- /dev/null +++ b/nvmet/__init__.py @@ -0,0 +1 @@ +from nvme import Root, Subsystem, Namespace diff --git a/nvmet/nvme.py b/nvmet/nvme.py new file mode 100644 index 0000000..fffdb0e --- /dev/null +++ b/nvmet/nvme.py @@ -0,0 +1,474 @@ +''' +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 "" % 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 "" % 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() diff --git a/nvmetcli b/nvmetcli new file mode 100755 index 0000000..0650cab --- /dev/null +++ b/nvmetcli @@ -0,0 +1,246 @@ +#!/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() diff --git a/setup.py b/setup.py new file mode 100755 index 0000000..93cdae1 --- /dev/null +++ b/setup.py @@ -0,0 +1,29 @@ +#! /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'], + )