summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-05-05 15:57:28 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-05-05 15:57:28 +0000
commita5283f584122bbcfb9085d46f6efe18d45440719 (patch)
tree6f516f7d908a9852650b2f48f50818e0c1a3b3e0
parentInitial commit. (diff)
downloadnvmetcli-a5283f584122bbcfb9085d46f6efe18d45440719.tar.xz
nvmetcli-a5283f584122bbcfb9085d46f6efe18d45440719.zip
Adding upstream version 0.8.upstream/0.8upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
-rw-r--r--.gitignore12
-rw-r--r--COPYING176
-rw-r--r--Documentation/Makefile31
-rw-r--r--Documentation/nvmetcli.txt267
-rw-r--r--Makefile130
-rw-r--r--README63
-rwxr-xr-xbump-ver.sh25
-rw-r--r--examples/fc.json42
-rw-r--r--examples/loop.json44
-rw-r--r--examples/rdma.json42
-rw-r--r--fc.json42
-rw-r--r--loop.json44
-rw-r--r--nvmet.service16
-rw-r--r--nvmet/__init__.py2
-rw-r--r--nvmet/nvme.py928
-rw-r--r--nvmet/test_nvmet.py484
-rwxr-xr-xnvmetcli754
-rw-r--r--rdma.json42
-rw-r--r--rpm/nvmetcli.spec.tmpl55
-rw-r--r--setup.cfg2
-rwxr-xr-xsetup.py31
-rw-r--r--tcp.json58
22 files changed, 3290 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..5a8946e
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,12 @@
+*.swp
+*.swo
+build-stamp
+build/*
+dist/*
+*.pyc
+*.pyc
+
+# generated documentation
+Documentation/*.1
+Documentation/*.xml
+Documentation/*.html
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/Documentation/Makefile b/Documentation/Makefile
new file mode 100644
index 0000000..8e0281c
--- /dev/null
+++ b/Documentation/Makefile
@@ -0,0 +1,31 @@
+PKGNAME = nvmetcli
+MANPAGE = ${PKGNAME}.8
+HTMLFILE = ${PKGNAME}.html
+XMLFILE = ${PKGNAME}.xml
+INSTALL ?= install
+PREFIX := /usr
+
+ASCIIDOC = asciidoc
+XMLTO = xmlto --skip-validation
+
+DOCDATA = ${XMLFILE} ${HTMLFILE}
+
+${MANPAGE}: ${DOCDATA}
+ ${XMLTO} man $<
+
+%.xml: %.txt
+ ${ASCIIDOC} -b docbook -d manpage -o $@ $<
+
+%.html: %.txt
+ ${ASCIIDOC} -a toc -o $@ $<
+
+installdoc: man8
+
+man8:
+ ${INSTALL} -m 644 ${MANPAGE} ${PREFIX}/share/man/man8
+
+uninstalldoc:
+ -rm -f ${PREFIX}/share/man/man8/${MANPAGE}
+
+clean:
+ -rm -f ${MANPAGE} ${HTMLFILE} ${XMLFILE}
diff --git a/Documentation/nvmetcli.txt b/Documentation/nvmetcli.txt
new file mode 100644
index 0000000..43e9f97
--- /dev/null
+++ b/Documentation/nvmetcli.txt
@@ -0,0 +1,267 @@
+nvmetcli(8)
+===========
+
+NAME
+----
+nvmetcli - Configure NVMe-over-Fabrics Target.
+
+USAGE
+------
+[verse]
+nvmetcli
+nvmetcli clear
+nvmetcli restore [filename.json]
+
+DESCRIPTION
+-----------
+*nvmetcli* is a program used for viewing, editing, saving,
+and starting a Linux kernel NVMe Target, used for an NVMe-over-Fabrics
+network configuration. It allows an administrator to export
+a storage resource (such as NVMe devices, files, and volumes)
+to a local block device and expose them to remote systems
+based on the NVMe-over-Fabrics specification from http://www.nvmexpress.org.
+
+*nvmetcli* is run as root and has two modes:
+
+1. An interactive configuration shell
+2. Command-line mode which uses an argument
+
+BACKGROUND
+----------
+The term *NQN* used throughout this man page is the *NVMe Qualified
+Name* format which an NVMe endpoint (device, subsystem, etc) must
+follow to guarantee a unique name under the NVMe standard. Any
+name in a network system setup can be used, but if it does not
+follow the NQN format, it may not be unique on an NVMe-over-Fabrics network.
+
+Note that some of the fields set for an NVMe Target port under
+interactive mode are defined in the "Discovery Log Page" section of
+NVMe-over-Fabrics specification. Each NVMe Target has a
+discovery controller mechanism that an NVMe Host can use to determine
+the NVM subsystems it can access. *nvmetcli* can be used to add
+a new record to the discovery controller upon each new subsystem
+entry and port entry that the newly created subsystem entry binds
+to (see *OPTIONS* and *EXAMPLES* sections). Each NVMe
+Host only gets to see the discovery entries defined in
+*/subsystems/[NQN NAME]/allowed_hosts* and the IP port it is connected
+to the NVMe Target. An NVMe Host can retrieve these discovery logs via
+the nvme-cli tool (https://github.com/linux-nvme/nvme-cli).
+
+OPTIONS
+-------
+*Interactive Configuration Shell*
+
+To start the interactive configuration shell, type *nvmetcli* on
+the command-line. nvmetcli interacts with the Linux kernel
+NVMe Target configfs subsystem starting at base
+nvmetcli directories **/port**, **/subsystem**, and **/host**.
+Configuration changes entered by the administrator are made
+immediately to the kernel target configuration. The
+following commands can be used while in the interactive configuration
+shell mode:
+[]
+|==================
+| cd | Allows to move around the tree.
+| ls | Lists contents of current tree node.
+| create [NQN name]/[#] | Create a new object using the specified name
+ or number. If a [NQN name]/[#] is not specified,
+ a random entry will be used.
+| delete [NQN name]/[#] | Delete an object with the specified name or number.
+| set attr allow_any_host=[0/1] | Used under */subsystems/[NQN name]* to
+ specify if any NVMe Host can connect to
+ the subsystem.
+| set device path=[device path] | Used under
+ */subsystems/[NQN name]/namespaces*
+ to set the (storage) device to be used.
+| set device nguid=[string] | Used under
+ */subsystems/[NQN name]/namespaces*
+ to set the unique id of the device to
+ the defined namespace.
+| enable/disable | Used under
+ */subsystems/[NQN name]/namespaces*
+ to enable and disable the namespace.
+| set addr [discovery log page field]=[string] | Used under */ports/[#]*
+ to create a port which
+ access is allowed. See
+ *EXAMPLES* for more
+ information.
+| saveconfig [filename.json] | Save the NVMe Target configuration in .json
+ format. Without specifying the
+ filename this will save as
+ */etc/nvmet/config.json*. This file
+ is in JSON format and can be edited directly
+ using a preferred file editor.
+| exit | Quits interactive configuration shell mode.
+|==================
+
+*Command Line Mode*
+
+Typing *nvmetcli [cmd]* on the command-line will execute a command
+and not enter the interactive configuration shell.
+
+[]
+|==================
+| restore [filename.json] | Loads a saved NVMe Target configuration.
+ Without specifying the filename this will use
+ */etc/nvmet/config.json*.
+| clear | Clears a current NVMe Target configuration.
+| ls | Dumps the current NVMe Target configuration.
+|==================
+
+EXAMPLES
+--------
+
+Make sure to run nvmetcli as root, the nvmet module is loaded,
+your devices and all dependent modules are loaded,
+and configfs is mounted on /sys/kernel/config
+using:
+
+ mount -t configfs none /sys/kernel/config
+
+The following section walks through a configuration example.
+
+* To get started with the interactive mode and the nvmetcli command prompt,
+type (in root):
+--------------
+# ./nvmetcli
+...>
+--------------
+
+* Create a subsystem. If you do not specify a name a NQN will be generated,
+which is probably the best choice. We don't do it here as the name
+would be random:
+--------------
+> cd /subsystems
+...> create testnqn
+--------------
+
+* Add access for a specific NVMe Host by it's NQN:
+--------------
+...> cd /hosts
+...> create hostnqn
+...> cd /subsystems/testnqn
+...> set attr allow_any_host=0
+...> cd /subsystems/testnqn/allowed_hosts/
+...> create hostnqn
+--------------
+
+* Remove access of a subsystem by deleting the Host NQN:
+--------------
+...> cd /subsystems/testnqn/allowed_hosts/
+...> delete hostnqn
+--------------
+
+* Alternatively this allows any Host to connect to the subsystsem. Only
+use this in tightly controlled 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:
+--------------
+...> cd /subsystems/testnqn/namespaces
+...> create 1
+...> cd 1
+...> set device path=/dev/nvme0n1
+...> enable
+--------------
+
+Note that in the above setup the 'device_nguid' attribute
+does not have to be set for correct NVMe Target functionality (but
+to correctly match a namespace to the exact device upon
+clear and restore operations, it is advised to set the
+'device_nguid' parameter).
+
+* Create a loopback port that can be used with nvme-loop module
+on the same physical machine...
+--------------
+...> cd /ports/
+...> create 1
+...> cd 1/
+...> set addr trtype=loop
+...> cd subsystems/
+...> create testnqn
+--------------
+
+* or create an RDMA (IB, RoCE, iWarp) port using IPv4 addressing. 4420 is the
+IANA assigned default port for NVMe over Fabrics using RDMA:
+--------------
+...> cd /ports/
+...> create 2
+...> cd 2/
+...> set addr trtype=rdma
+...> set addr adrfam=ipv4
+...> set addr traddr=192.168.6.68
+...> set addr trsvcid=4420
+...> cd subsystems/
+...> create testnqn
+--------------
+
+* or create an FC port. traddr is the WWNN/WWPN of the FC port.
+--------------
+...> cd /ports/
+...> create 3
+...> cd 3/
+...> set addr trtype=fc
+...> set addr adrfam=fc
+...> set addr traddr=nn-0x1000000044001123:pn-0x2000000055001123
+...> set addr trsvcid=none
+...> cd subsystems/
+...> create testnqn
+--------------
+
+* Saving the NVMe Target configuration:
+--------------
+./nvmetcli
+...> saveconfig test.json
+--------------
+
+* Loading an NVMe Target configuration:
+--------------
+ ./nvmetcli restore test.json
+--------------
+
+* Clearing a current NVMe Target configuration:
+--------------
+ ./nvmetcli clear
+--------------
+
+ADDITIONAL INFORMATION
+----------------------
+nvmetcli has the ability to start and stop the NVMe Target configuration
+on boot and shutdown through the *systemctl* Linux utility via a .service file.
+nvmetcli package comes with *nvmet.service* which when installed, it can
+automatically restore the default, saved NVMe Target configuration from
+*/etc/nvmet/config.json*. *nvmet.service* can be installed in directories
+such as */lib/systemd/system*.
+
+To explicitly enable the service, type:
+--------------
+ systemctl enable nvmet
+--------------
+
+To explicitly disable the service, type:
+--------------
+ systemctl disable nvmet
+--------------
+
+See also systemctl(1).
+
+AUTHORS
+-------
+This man page was written by
+mailto:james.p.freyensee@intel.com[Jay Freyensee]. nvmetcli was
+originally written by mailto:hch@infradead.org[Christoph Hellwig].
+
+REPORTING BUGS & DEVELOPMENT
+-----------------------------
+Please send patches and bug reports to linux-nvme@lists.infradead.org
+for review and acceptance.
+
+LICENSE
+-------
+nvmetcli is licensed under the *Apache License, Version 2.0*. Software
+distributed under this license is distributed on an "AS IS" BASIS, WITHOUT
+WARRANTIES OR CONDITIONS OF ANY KIND, either expressed or implied.
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..092d53c
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,130 @@
+PKGNAME = nvmetcli
+NAME = nvmet
+GIT_BRANCH = $$(git branch | grep \* | tr -d \*)
+VERSION = $$(basename $$(git describe --tags | tr - . | sed 's/^v//'))
+DOCDIR = ./Documentation
+
+all:
+ @echo "Usage:"
+ @echo
+ @echo " make deb - Builds debian packages."
+ @echo " make rpm - Builds rpm packages."
+ @echo " make release - Generates the release tarball."
+ @echo " make doc - Builds manpages & html docs in ${DOCDIR}."
+ @echo
+ @echo " make clean - Cleanup the local repository build files."
+ @echo " make cleandoc - Cleanup auto-generated docs in ${DOCDIR}."
+ @echo " make cleanall - Remove dist/*, build files, auto-gen docs."
+ @echo " make installdoc - Install man pages (need sudo)."
+ @echo " make uninstalldoc - Uninstall man pages (need sudo)."
+
+test:
+ @nose2 -C --coverage ./nvmet
+
+doc: ${NAME}
+ ${MAKE} -C ${DOCDIR}
+
+installdoc:
+ ${MAKE} -C ${DOCDIR} installdoc
+
+uninstalldoc:
+ ${MAKE} -C ${DOCDIR} uninstalldoc
+
+cleandoc:
+ ${MAKE} -C ${DOCDIR} clean
+
+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 cleandoc
+ @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
+ @echo "Generating debian changelog..."
+ @( \
+ version=${VERSION}; \
+ author=$$(git show HEAD --format="format:%an <%ae>" -s); \
+ date=$$(git show HEAD --format="format:%aD" -s); \
+ day=$$(git show HEAD --format='format:%ai' -s \
+ | awk '{print $$1}' \
+ | awk -F '-' '{print $$3}' | sed 's/^0/ /g'); \
+ date=$$(echo $${date} \
+ | awk '{print $$1, "'"$${day}"'", $$3, $$4, $$5, $$6}'); \
+ hash=$$(git show HEAD --format="format:%H" -s); \
+ echo "${PKGNAME} ($${version}) unstable; urgency=low"; \
+ echo; \
+ echo " * Generated from git commit $${hash}."; \
+ echo; \
+ echo " -- $${author} $${date}"; \
+ echo; \
+ ) > build/${PKGNAME}-${VERSION}/debian/changelog
+ @echo "Generating rpm specfile from template..."
+ @cd build/${PKGNAME}-${VERSION}; \
+ for spectmpl in rpm/*.spec.tmpl; do \
+ sed -i "s/Version:.*/Version: ${VERSION}/g" $${spectmpl}; \
+ mv $${spectmpl} $$(basename $${spectmpl} .tmpl); \
+ done; \
+ rmdir rpm
+ @echo "Generating rpm changelog..."
+ @( \
+ version=${VERSION}; \
+ author=$$(git show HEAD --format="format:%an <%ae>" -s); \
+ date=$$(git show HEAD --format="format:%ad" -s \
+ | awk '{print $$1,$$2,$$3,$$5}'); \
+ hash=$$(git show HEAD --format="format:%H" -s); \
+ echo '* '"$${date} $${author} $${version}-1"; \
+ echo " - Generated from git commit $${hash}."; \
+ ) >> $$(ls build/${PKGNAME}-${VERSION}/*.spec)
+ @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
+
+deb: release build/deb-stamp
+build/deb-stamp:
+ @echo "Building debian packages..."
+ @cd build/${PKGNAME}-${VERSION}; \
+ dpkg-buildpackage -rfakeroot -us -uc
+ @mv build/*_${VERSION}_*.deb dist/
+ @echo "Generated debian packages:"
+ @for pkg in $$(ls dist/*_${VERSION}_*.deb); do echo " $${pkg}"; done
+ @touch build/deb-stamp
+
+rpm: release build/rpm-stamp
+build/rpm-stamp:
+ @echo "Building rpm packages..."
+ @mkdir -p build/rpm
+ @build=$$(pwd)/build/rpm; dist=$$(pwd)/dist/; rpmbuild \
+ --define "_topdir $${build}" --define "_sourcedir $${dist}" \
+ --define "_rpmdir $${build}" --define "_buildir $${build}" \
+ --define "_srcrpmdir $${build}" -ba build/${PKGNAME}-${VERSION}/*.spec
+ @mv build/rpm/*-${VERSION}*.src.rpm dist/
+ @mv build/rpm/*/*-${VERSION}*.rpm dist/
+ @echo "Generated rpm packages:"
+ @for pkg in $$(ls dist/*-${VERSION}*.rpm); do echo " $${pkg}"; done
+ @touch build/rpm-stamp
diff --git a/README b/README
new file mode 100644
index 0000000..6ebe666
--- /dev/null
+++ b/README
@@ -0,0 +1,63 @@
+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/open-iscsi/configshell-fb first.
+
+nvmetcli can be run directly from the source directory or installed
+using setup.py.
+
+Common Package Dependencies and Problems
+-----------------------------------------
+Both python2 and python3 are supported via use of the 'python-six'
+package.
+
+nvmetcli uses the 'pyparsing' package -- running nvmetcli without this
+package may produce hard-to-decipher errors.
+
+Usage
+-----
+Look at Documentation/nvmetcli.txt for details.
+
+Example NVMe Target .json files
+--------------------------------------
+To load the loop + explicit host version above do the following:
+
+ ./nvmetcli restore loop.json
+
+Or to load the rdma + no host authentication version do the following
+after you've ensured that the IP address in rdma.json fits your setup:
+
+ ./nvmetcli restore rdma.json
+
+Or to load the fc + no host authentication version do the following
+after you've ensured that the port traddr FC address information in
+fc.json fits your setup:
+
+ ./nvmetcli restore fc.json
+
+Or to load the tcp + no host authentication version do the following
+after you've ensured that the IP address in tcp.json fits your setup:
+
+ ./nvmetcli restore tcp.json
+
+These files can also be edited directly using your favorite editor.
+
+Testing
+-------
+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'. To run all
+the tests you also need some test block devices or files. Default is to
+use /dev/ram0 and /dev/ram1. You can override default with environmental
+variable eg. NVMET_TEST_DEVICES="/dev/sdk,/dev/sdj" make test .
+
+Development
+-----------------
+Please send patches and bug reports to linux-nvme@lists.infradead.org for
+review and acceptance.
diff --git a/bump-ver.sh b/bump-ver.sh
new file mode 100755
index 0000000..21f7df3
--- /dev/null
+++ b/bump-ver.sh
@@ -0,0 +1,25 @@
+#!/bin/sh
+#
+# Bump the version and cut a release. Can't be called release.sh because
+# that would conflict with the make release rule in the makefile.
+#
+
+VER=$1
+
+set -e
+set +x
+
+if [ -z "$VER" ]; then
+ echo "usage: $0 version" >&2
+ exit 1
+fi
+
+make clean
+sed -i "s/version =.*,/version = $VER,/" setup.py
+git add setup.py
+git commit -m "bump version to v$VER"
+
+git tag -s "v$VER" -m "nvmetcli release v$VER"
+
+make release
+(cd dist && gpg --armor --detach-sign nvmetcli-$VER.tar.gz)
diff --git a/examples/fc.json b/examples/fc.json
new file mode 100644
index 0000000..e120aef
--- /dev/null
+++ b/examples/fc.json
@@ -0,0 +1,42 @@
+{
+ "hosts": [
+ {
+ "nqn": "hostnqn"
+ }
+ ],
+ "ports": [
+ {
+ "addr": {
+ "adrfam": "fc",
+ "traddr": "nn-0x1000000044001123:pn-0x2000000055001123",
+ "treq": "not specified",
+ "trsvcid": "none",
+ "trtype": "fc"
+ },
+ "portid": 3,
+ "referrals": [],
+ "subsystems": [
+ "testnqn"
+ ]
+ }
+ ],
+ "subsystems": [
+ {
+ "allowed_hosts": [],
+ "attr": {
+ "allow_any_host": "1"
+ },
+ "namespaces": [
+ {
+ "device": {
+ "nguid": "ef90689c-6c46-d44c-89c1-4067801309a8",
+ "path": "/dev/nvme0n1"
+ },
+ "enable": 1,
+ "nsid": 1
+ }
+ ],
+ "nqn": "testnqn"
+ }
+ ]
+}
diff --git a/examples/loop.json b/examples/loop.json
new file mode 100644
index 0000000..a2b7ec7
--- /dev/null
+++ b/examples/loop.json
@@ -0,0 +1,44 @@
+{
+ "hosts": [
+ {
+ "nqn": "hostnqn"
+ }
+ ],
+ "ports": [
+ {
+ "addr": {
+ "adrfam": "",
+ "traddr": "",
+ "treq": "not specified",
+ "trsvcid": "",
+ "trtype": "loop"
+ },
+ "portid": 1,
+ "referrals": [],
+ "subsystems": [
+ "testnqn"
+ ]
+ }
+ ],
+ "subsystems": [
+ {
+ "allowed_hosts": [
+ "hostnqn"
+ ],
+ "attr": {
+ "allow_any_host": "0"
+ },
+ "namespaces": [
+ {
+ "device": {
+ "nguid": "ef90689c-6c46-d44c-89c1-4067801309a8",
+ "path": "/dev/nvme0n1"
+ },
+ "enable": 1,
+ "nsid": 1
+ }
+ ],
+ "nqn": "testnqn"
+ }
+ ]
+}
diff --git a/examples/rdma.json b/examples/rdma.json
new file mode 100644
index 0000000..6fc326c
--- /dev/null
+++ b/examples/rdma.json
@@ -0,0 +1,42 @@
+{
+ "hosts": [
+ {
+ "nqn": "hostnqn"
+ }
+ ],
+ "ports": [
+ {
+ "addr": {
+ "adrfam": "ipv4",
+ "traddr": "192.168.6.68",
+ "treq": "not specified",
+ "trsvcid": "4420",
+ "trtype": "rdma"
+ },
+ "portid": 2,
+ "referrals": [],
+ "subsystems": [
+ "testnqn"
+ ]
+ }
+ ],
+ "subsystems": [
+ {
+ "allowed_hosts": [],
+ "attr": {
+ "allow_any_host": "1"
+ },
+ "namespaces": [
+ {
+ "device": {
+ "nguid": "ef90689c-6c46-d44c-89c1-4067801309a8",
+ "path": "/dev/nvme0n1"
+ },
+ "enable": 1,
+ "nsid": 1
+ }
+ ],
+ "nqn": "testnqn"
+ }
+ ]
+}
diff --git a/fc.json b/fc.json
new file mode 100644
index 0000000..e120aef
--- /dev/null
+++ b/fc.json
@@ -0,0 +1,42 @@
+{
+ "hosts": [
+ {
+ "nqn": "hostnqn"
+ }
+ ],
+ "ports": [
+ {
+ "addr": {
+ "adrfam": "fc",
+ "traddr": "nn-0x1000000044001123:pn-0x2000000055001123",
+ "treq": "not specified",
+ "trsvcid": "none",
+ "trtype": "fc"
+ },
+ "portid": 3,
+ "referrals": [],
+ "subsystems": [
+ "testnqn"
+ ]
+ }
+ ],
+ "subsystems": [
+ {
+ "allowed_hosts": [],
+ "attr": {
+ "allow_any_host": "1"
+ },
+ "namespaces": [
+ {
+ "device": {
+ "nguid": "ef90689c-6c46-d44c-89c1-4067801309a8",
+ "path": "/dev/nvme0n1"
+ },
+ "enable": 1,
+ "nsid": 1
+ }
+ ],
+ "nqn": "testnqn"
+ }
+ ]
+}
diff --git a/loop.json b/loop.json
new file mode 100644
index 0000000..a2b7ec7
--- /dev/null
+++ b/loop.json
@@ -0,0 +1,44 @@
+{
+ "hosts": [
+ {
+ "nqn": "hostnqn"
+ }
+ ],
+ "ports": [
+ {
+ "addr": {
+ "adrfam": "",
+ "traddr": "",
+ "treq": "not specified",
+ "trsvcid": "",
+ "trtype": "loop"
+ },
+ "portid": 1,
+ "referrals": [],
+ "subsystems": [
+ "testnqn"
+ ]
+ }
+ ],
+ "subsystems": [
+ {
+ "allowed_hosts": [
+ "hostnqn"
+ ],
+ "attr": {
+ "allow_any_host": "0"
+ },
+ "namespaces": [
+ {
+ "device": {
+ "nguid": "ef90689c-6c46-d44c-89c1-4067801309a8",
+ "path": "/dev/nvme0n1"
+ },
+ "enable": 1,
+ "nsid": 1
+ }
+ ],
+ "nqn": "testnqn"
+ }
+ ]
+}
diff --git a/nvmet.service b/nvmet.service
new file mode 100644
index 0000000..6f97a91
--- /dev/null
+++ b/nvmet.service
@@ -0,0 +1,16 @@
+[Unit]
+Description=Restore NVMe kernel target configuration
+Requires=sys-kernel-config.mount
+After=sys-kernel-config.mount network-online.target local-fs.target
+Wants=network-online.target
+
+[Service]
+Type=oneshot
+RemainAfterExit=yes
+ExecStart=/usr/sbin/nvmetcli restore
+ExecStop=/usr/sbin/nvmetcli clear
+SyslogIdentifier=nvmetcli
+
+[Install]
+WantedBy=multi-user.target
+
diff --git a/nvmet/__init__.py b/nvmet/__init__.py
new file mode 100644
index 0000000..cf172bd
--- /dev/null
+++ b/nvmet/__init__.py
@@ -0,0 +1,2 @@
+from .nvme import Root, Subsystem, Namespace, Port, Host, Referral, ANAGroup,\
+ DEFAULT_SAVE_FILE
diff --git a/nvmet/nvme.py b/nvmet/nvme.py
new file mode 100644
index 0000000..59efdb5
--- /dev/null
+++ b/nvmet/nvme.py
@@ -0,0 +1,928 @@
+'''
+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
+from six import iteritems, moves
+
+DEFAULT_SAVE_FILE = '/etc/nvmet/config.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 configfs, 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._enable = None
+ 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__)
+ self.get_enable()
+
+ 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)[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)
+
+ if self._enable:
+ raise CFSError("Cannot set attribute while %s is enabled" %
+ self.__class__.__name__)
+
+ 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 get_enable(self):
+ self._check_self()
+ path = "%s/enable" % self.path
+ if not os.path.isfile(path):
+ return None
+
+ with open(path, 'r') as file_fd:
+ self._enable = int(file_fd.read().strip())
+ return self._enable
+
+ def set_enable(self, value):
+ self._check_self()
+ path = "%s/enable" % self.path
+
+ if not os.path.isfile(path) or self._enable is None:
+ raise CFSError("Cannot enable %s" % self.path)
+
+ try:
+ with open(path, 'w') as file_fd:
+ file_fd.write(str(value))
+ except Exception as e:
+ raise CFSError("Cannot enable %s: %s (%s)" %
+ (self.path, e, value))
+ self._enable = value
+
+ 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
+ if self._enable is not None:
+ d['enable'] = self._enable
+ return d
+
+ def _setup_attrs(self, attr_dict, err_func):
+ for group in self.attr_groups:
+ for name, value in iteritems(attr_dict.get(group, {})):
+ try:
+ self.set_attr(group, name, value)
+ except CFSError as e:
+ err_func(str(e))
+ enable = attr_dict.get('enable')
+ if enable is not None:
+ self.set_enable(enable)
+
+
+class Root(CFSNode):
+ def __init__(self):
+ super(Root, self).__init__()
+
+ if not os.path.isdir(self.configfs_dir):
+ self._modprobe('nvmet')
+
+ if not os.path.isdir(self.configfs_dir):
+ raise CFSError("%s does not exist. Giving up." %
+ self.configfs_dir)
+
+ self._path = self.configfs_dir
+ self._create_in_cfs('lookup')
+
+ def _modprobe(self, modname):
+ try:
+ from kmodpy import kmod
+
+ try:
+ kmod.Kmod().modprobe(modname, quiet=True)
+ except kmod.KmodError:
+ pass
+ except ImportError:
+ # Try the ctypes library included with the libkmod itself.
+ try:
+ import kmod
+ kmod.Kmod().modprobe(modname)
+ except Exception as e:
+ pass
+
+ 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 _list_ports(self):
+ self._check_self()
+
+ for d in os.listdir("%s/ports/" % self._path):
+ yield Port(d, 'lookup')
+
+ 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.
+ '''
+ if savefile:
+ savefile = os.path.expanduser(savefile)
+ else:
+ savefile = DEFAULT_SAVE_FILE
+
+ savefile_abspath = os.path.abspath(savefile)
+ savefile_dir = os.path.dirname(savefile_abspath)
+ if not os.path.exists(savefile_dir):
+ os.makedirs(savefile_dir)
+
+ 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")
+ f.flush()
+ os.fsync(f.fileno())
+
+ os.rename(savefile + ".temp", savefile)
+
+ # Sync the containing directory too
+ dir_fd = None
+ try:
+ dir_fd = os.open(savefile_dir, os.O_RDONLY)
+ os.fsync(dir_fd)
+ finally:
+ if dir_fd:
+ os.close(dir_fd)
+
+ def clear_existing(self):
+ '''
+ Remove entire current configuration.
+ '''
+
+ for p in self.ports:
+ p.delete()
+ for s in self.subsystems:
+ s.delete()
+ for h in self.hosts:
+ h.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")
+
+ # 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)
+ continue
+
+ 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,
+ 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]
+ d['ports'] = [p.dump() for p in self.ports]
+ d['hosts'] = [h.dump() for h in self.hosts]
+ 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 "<Subsystem %s>" % self.nqn
+
+ def __init__(self, nqn=None, mode='any'):
+ '''
+ @param nqn: The Subsystems' NQN.
+ If no NQN is specified, one will be generated.
+ @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 Subsystem object.
+ '''
+ super(Subsystem, self).__init__()
+
+ if nqn is None:
+ if mode == 'lookup':
+ raise CFSError("Need NQN for lookup")
+ 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)
+
+ def _generate_nqn(self):
+ prefix = "nqn.2014-08.org.nvmexpress:NVMf:uuid"
+ name = str(uuid.uuid4())
+ return "%s:%s" % (prefix, name)
+
+ def delete(self):
+ '''
+ Recursively deletes a Subsystem object.
+ This will delete all attached Namespace objects and then the
+ Subsystem itself.
+ '''
+ 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()
+
+ def _list_namespaces(self):
+ self._check_self()
+ for d in os.listdir("%s/namespaces/" % self._path):
+ yield Namespace(self, int(d), 'lookup')
+
+ 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):
+ '''
+ Set up Subsystem 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)
+ 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
+
+
+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 "<Namespace %d>" % self.nsid
+
+ def __init__(self, subsystem, nsid=None, mode='any'):
+ '''
+ @param subsystem: The parent Subsystem object
+ @param nsid: The Namespace identifier
+ If no nsid is specified, the next free one will be used.
+ @type nsid: int
+ @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 Namespace object.
+ '''
+ super(Namespace, self).__init__()
+
+ if not isinstance(subsystem, Subsystem):
+ raise CFSError("Invalid parent class")
+
+ if nsid is None:
+ if mode == 'lookup':
+ raise CFSError("Need NSID for lookup")
+
+ nsids = [n.nsid for n in subsystem.namespaces]
+ for index in moves.xrange(1, self.MAX_NSID + 1):
+ if index not in nsids:
+ nsid = index
+ break
+ if nsid is None:
+ raise CFSError("All NSIDs 1-%d in use" % self.MAX_NSID)
+ else:
+ nsid = int(nsid)
+ if nsid < 1 or nsid > self.MAX_NSID:
+ raise CFSError("NSID must be 1 to %d" % self.MAX_NSID)
+
+ self.attr_groups = ['device', 'ana']
+ 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
+
+ def _get_grpid(self):
+ self._check_self()
+ _grpid = 0
+ path = "%s/ana_grpid" % self.path
+ if os.path.isfile(path):
+ with open(path, 'r') as file_fd:
+ _grpid = int(file_fd.read().strip())
+ return _grpid
+
+ def set_grpid(self, grpid):
+ self._check_self()
+ path = "%s/ana_grpid" % self.path
+ if os.path.isfile(path):
+ with open(path, 'w') as file_fd:
+ file_fd.write(str(grpid))
+
+ grpid = property(_get_grpid, doc="Get the ANA Group ID.")
+
+ 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)
+ if 'ana_grpid' in n:
+ ns.set_grpid(int(n['ana_grpid']))
+
+ def dump(self):
+ d = super(Namespace, self).dump()
+ d['nsid'] = self.nsid
+ d['ana_grpid'] = self.grpid
+ return d
+
+
+class Port(CFSNode):
+ '''
+ This is an interface to a NVMe Port in configFS.
+ '''
+
+ MAX_PORTID = 8192
+
+ def __repr__(self):
+ return "<Port %d>" % self.portid
+
+ def __init__(self, portid, mode='any'):
+ super(Port, self).__init__()
+
+ self.attr_groups = ['addr', 'param']
+ self._portid = int(portid)
+ self._path = "%s/ports/%d" % (self.configfs_dir, self._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.")
+
+ def _list_subsystems(self):
+ return [os.path.basename(name)
+ for name in os.listdir("%s/subsystems/" % self._path)]
+
+ subsystems = property(_list_subsystems,
+ doc="Get the list of Subsystem for this Port.")
+
+ def add_subsystem(self, nqn):
+ '''
+ Enable access to the Subsystem identified by I{nqn} through this Port.
+ '''
+ try:
+ os.symlink("%s/subsystems/%s" % (self.configfs_dir, nqn),
+ "%s/subsystems/%s" % (self._path, nqn))
+ except Exception as e:
+ raise CFSError("Could not symlink %s in configFS: %s" % (nqn, e))
+
+ def remove_subsystem(self, nqn):
+ '''
+ Disable access to the Subsystem identified by I{nqn} through this Port.
+ '''
+ try:
+ os.unlink("%s/subsystems/%s" % (self._path, nqn))
+ except Exception as e:
+ raise CFSError("Could not unlink %s in configFS: %s" % (nqn, e))
+
+ def delete(self):
+ '''
+ Recursively deletes a Port object.
+ '''
+ self._check_self()
+ for s in self.subsystems:
+ self.remove_subsystem(s)
+ for a in self.ana_groups:
+ a.delete()
+ for r in self.referrals:
+ r.delete()
+ super(Port, self).delete()
+
+ def _list_referrals(self):
+ self._check_self()
+ for d in os.listdir("%s/referrals/" % self._path):
+ yield Referral(self, d, 'lookup')
+
+ referrals = property(_list_referrals,
+ doc="Get the list of Referrals for this Port.")
+
+ def _list_ana_groups(self):
+ self._check_self()
+ if os.path.isdir("%s/ana_groups/" % self._path):
+ for d in os.listdir("%s/ana_groups/" % self._path):
+ yield ANAGroup(self, int(d), 'lookup')
+
+ ana_groups = property(_list_ana_groups,
+ doc="Get the list of ANA Groups for this Port.")
+
+ @classmethod
+ def setup(cls, root, n, err_func):
+ '''
+ Set up a Port 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(n['portid'])
+ except CFSError as e:
+ err_func("Could not create Port object: %s" % e)
+ return
+
+ port._setup_attrs(n, err_func)
+ for s in n.get('subsystems', []):
+ port.add_subsystem(s)
+ for a in n.get('ana_groups', []):
+ ANAGroup.setup(port, a, err_func)
+ for r in n.get('referrals', []):
+ Referral.setup(port, r, err_func)
+
+ def dump(self):
+ d = super(Port, self).dump()
+ d['portid'] = self.portid
+ d['subsystems'] = self.subsystems
+ d['ana_groups'] = [a.dump() for a in self.ana_groups]
+ d['referrals'] = [r.dump() for r in self.referrals]
+ return d
+
+
+class Referral(CFSNode):
+ '''
+ This is an interface to a NVMe Referral in configFS.
+ '''
+
+ def __repr__(self):
+ return "<Referral %d>" % self.name
+
+ def __init__(self, port, name, mode='any'):
+ super(Referral, self).__init__()
+
+ if not isinstance(port, Port):
+ raise CFSError("Invalid parent class")
+
+ self.attr_groups = ['addr']
+ self.port = port
+ self._name = name
+ self._path = "%s/referrals/%s" % (self.port.path, self._name)
+ self._create_in_cfs(mode)
+
+ def _get_name(self):
+ return self._name
+
+ name = property(_get_name, doc="Get the Referral name.")
+
+ @classmethod
+ def setup(cls, port, n, err_func):
+ '''
+ Set up a Referral based upon n dict, from saved config.
+ Guard against missing or bad dict items, but keep going.
+ Call 'err_func' for each error.
+ '''
+
+ if 'name' not in n:
+ err_func("'name' not defined for Referral")
+ return
+
+ try:
+ r = Referral(port, n['name'])
+ except CFSError as e:
+ err_func("Could not create Referral object: %s" % e)
+ return
+
+ r._setup_attrs(n, err_func)
+
+ def dump(self):
+ d = super(Referral, self).dump()
+ d['name'] = self.name
+ return d
+
+
+class ANAGroup(CFSNode):
+ '''
+ This is an interface to a NVMe ANA Group in configFS.
+ '''
+
+ MAX_GRPID = 1024
+
+ def __repr__(self):
+ return "<ANA Group %d>" % self.grpid
+
+ def __init__(self, port, grpid, mode='any'):
+ super(ANAGroup, self).__init__()
+
+ if not os.path.isdir("%s/ana_groups" % port.path):
+ raise CFSError("ANA not supported")
+
+ if grpid is None:
+ if mode == 'lookup':
+ raise CFSError("Need grpid for lookup")
+
+ grpids = [n.grpid for n in port.ana_groups]
+ for index in moves.xrange(2, self.MAX_GRPID + 1):
+ if index not in grpids:
+ grpid = index
+ break
+ if grpid is None:
+ raise CFSError("All ANA Group IDs 1-%d in use" % self.MAX_GRPID)
+ else:
+ grpid = int(grpid)
+ if grpid < 1 or grpid > self.MAX_GRPID:
+ raise CFSError("GRPID %d must be 1 to %d" % (grpid, self.MAX_GRPID))
+
+ self.attr_groups = ['ana']
+ self._port = port
+ self._grpid = grpid
+ self._path = "%s/ana_groups/%d" % (self._port.path, self.grpid)
+ self._create_in_cfs(mode)
+
+ def _get_grpid(self):
+ return self._grpid
+
+ grpid = property(_get_grpid, doc="Get the ANA Group ID.")
+
+ @classmethod
+ def setup(cls, port, n, err_func):
+ '''
+ Set up an ANA Group 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 'grpid' not in n:
+ err_func("'grpid' not defined for ANA Group")
+ return
+
+ try:
+ a = ANAGroup(port, n['grpid'])
+ except CFSError as e:
+ err_func("Could not create ANA Group object: %s" % e)
+ return
+
+ a._setup_attrs(n, err_func)
+
+ def delete(self):
+ # ANA Group 1 is automatically created/deleted
+ if self.grpid != 1:
+ super(ANAGroup, self).delete()
+
+ def dump(self):
+ d = super(ANAGroup, self).dump()
+ d['grpid'] = self.grpid
+ 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()
+
+if __name__ == "__main__":
+ _test()
diff --git a/nvmet/test_nvmet.py b/nvmet/test_nvmet.py
new file mode 100644
index 0000000..13e2f6c
--- /dev/null
+++ b/nvmet/test_nvmet.py
@@ -0,0 +1,484 @@
+
+import os
+import random
+import stat
+import string
+import unittest
+import nvmet.nvme as nvme
+
+# Default test devices are ram disks, but allow user to specify different
+# block devices or files.
+NVMET_TEST_DEVICES = os.getenv("NVMET_TEST_DEVICES",
+ "/dev/ram0,/dev/ram1").split(',')
+
+
+def test_devices_present():
+ return len([x for x in NVMET_TEST_DEVICES
+ if os.path.exists(x) and
+ (stat.S_ISBLK(os.stat(x).st_mode) or os.path.isfile(x))]) >= 2
+
+
+class TestNvmet(unittest.TestCase):
+ def test_subsystem(self):
+ root = nvme.Root()
+ root.clear_existing()
+ for s in root.subsystems:
+ self.assertTrue(False, 'Found Subsystem after clear')
+
+ # create mode
+ s1 = nvme.Subsystem(nqn='testnqn1', mode='create')
+ self.assertIsNotNone(s1)
+ self.assertEqual(len(list(root.subsystems)), 1)
+
+ # any mode, should create
+ s2 = nvme.Subsystem(nqn='testnqn2', mode='any')
+ self.assertIsNotNone(s2)
+ self.assertEqual(len(list(root.subsystems)), 2)
+
+ # random name
+ s3 = nvme.Subsystem(mode='create')
+ self.assertIsNotNone(s3)
+ self.assertEqual(len(list(root.subsystems)), 3)
+
+ # duplicate
+ self.assertRaises(nvme.CFSError, nvme.Subsystem,
+ nqn='testnqn1', mode='create')
+ self.assertEqual(len(list(root.subsystems)), 3)
+
+ # lookup using any, should not create
+ s = nvme.Subsystem(nqn='testnqn1', mode='any')
+ self.assertEqual(s1, s)
+ self.assertEqual(len(list(root.subsystems)), 3)
+
+ # lookup only
+ s = nvme.Subsystem(nqn='testnqn2', mode='lookup')
+ self.assertEqual(s2, s)
+ self.assertEqual(len(list(root.subsystems)), 3)
+
+ # lookup without nqn
+ self.assertRaises(nvme.CFSError, nvme.Subsystem, mode='lookup')
+
+ # and delete them all
+ for s in root.subsystems:
+ s.delete()
+ self.assertEqual(len(list(root.subsystems)), 0)
+
+ def test_namespace(self):
+ root = nvme.Root()
+ root.clear_existing()
+
+ s = nvme.Subsystem(nqn='testnqn', mode='create')
+ for n in s.namespaces:
+ self.assertTrue(False, 'Found Namespace in new Subsystem')
+
+ # create mode
+ n1 = nvme.Namespace(s, nsid=3, mode='create')
+ self.assertIsNotNone(n1)
+ self.assertEqual(len(list(s.namespaces)), 1)
+
+ # any mode, should create
+ n2 = nvme.Namespace(s, nsid=2, mode='any')
+ self.assertIsNotNone(n2)
+ self.assertEqual(len(list(s.namespaces)), 2)
+
+ # create without nsid, should pick lowest available
+ n3 = nvme.Namespace(s, mode='create')
+ self.assertIsNotNone(n3)
+ self.assertEqual(n3.nsid, 1)
+ self.assertEqual(len(list(s.namespaces)), 3)
+
+ n4 = nvme.Namespace(s, mode='create')
+ self.assertIsNotNone(n4)
+ self.assertEqual(n4.nsid, 4)
+ self.assertEqual(len(list(s.namespaces)), 4)
+
+ # duplicate
+ self.assertRaises(nvme.CFSError, nvme.Namespace, 1, mode='create')
+ self.assertEqual(len(list(s.namespaces)), 4)
+
+ # lookup using any, should not create
+ n = nvme.Namespace(s, nsid=3, mode='any')
+ self.assertEqual(n1, n)
+ self.assertEqual(len(list(s.namespaces)), 4)
+
+ # lookup only
+ n = nvme.Namespace(s, nsid=2, mode='lookup')
+ self.assertEqual(n2, n)
+ self.assertEqual(len(list(s.namespaces)), 4)
+
+ # lookup without nsid
+ self.assertRaises(nvme.CFSError, nvme.Namespace, None, mode='lookup')
+
+ # and delete them all
+ for n in s.namespaces:
+ n.delete()
+ self.assertEqual(len(list(s.namespaces)), 0)
+
+ @unittest.skipUnless(test_devices_present(),
+ "Devices %s not available or suitable" % ','.join(NVMET_TEST_DEVICES))
+ def test_namespace_attrs(self):
+ root = nvme.Root()
+ root.clear_existing()
+
+ s = nvme.Subsystem(nqn='testnqn', mode='create')
+ n = nvme.Namespace(s, mode='create')
+
+ self.assertFalse(n.get_enable())
+ self.assertTrue('device' in n.attr_groups)
+ self.assertTrue('path' in n.list_attrs('device'))
+
+ # no device set yet, should fail
+ self.assertRaises(nvme.CFSError, n.set_enable, 1)
+
+ # now set a path and enable
+ n.set_attr('device', 'path', NVMET_TEST_DEVICES[0])
+ n.set_enable(1)
+ self.assertTrue(n.get_enable())
+
+ # test double enable
+ n.set_enable(1)
+
+ # test that we can't write to attrs while enabled
+ self.assertRaises(nvme.CFSError, n.set_attr, 'device', 'path',
+ NVMET_TEST_DEVICES[1])
+ self.assertRaises(nvme.CFSError, n.set_attr, 'device', 'nguid',
+ '15f7767b-50e7-4441-949c-75b99153dea7')
+
+ # disable: once and twice
+ n.set_enable(0)
+ n.set_enable(0)
+
+ # enable again, and remove while enabled
+ n.set_enable(1)
+ n.delete()
+
+ def test_recursive_delete(self):
+ root = nvme.Root()
+ root.clear_existing()
+
+ s = nvme.Subsystem(nqn='testnqn', mode='create')
+ n1 = nvme.Namespace(s, mode='create')
+ n2 = nvme.Namespace(s, mode='create')
+
+ 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(portid=0, mode='create')
+ self.assertIsNotNone(p1)
+ self.assertEqual(len(list(root.ports)), 1)
+
+ # any mode, should create
+ p2 = nvme.Port(portid=1, mode='any')
+ self.assertIsNotNone(p2)
+ self.assertEqual(len(list(root.ports)), 2)
+
+ # duplicate
+ self.assertRaises(nvme.CFSError, nvme.Port,
+ portid=0, mode='create')
+ self.assertEqual(len(list(root.ports)), 2)
+
+ # lookup using any, should not create
+ p = nvme.Port(portid=0, mode='any')
+ self.assertEqual(p1, p)
+ self.assertEqual(len(list(root.ports)), 2)
+
+ # lookup only
+ p = nvme.Port(portid=1, mode='lookup')
+ self.assertEqual(p2, p)
+ self.assertEqual(len(list(root.ports)), 2)
+
+ # 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()
+
+ s = nvme.Subsystem(nqn='testnqn', mode='create')
+ p = nvme.Port(portid=0, mode='create')
+
+ # subsystem doesn't exists, should fail
+ self.assertRaises(nvme.CFSError, p.add_subsystem, 'invalidnqn')
+
+ self.assertTrue('addr' in p.attr_groups)
+
+ # no trtype set yet, should fail
+ self.assertRaises(nvme.CFSError, p.add_subsystem, 'testnqn')
+
+ # 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.add_subsystem('testnqn')
+
+ # test double add
+ self.assertRaises(nvme.CFSError, p.add_subsystem, 'testnqn')
+
+ # 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')
+
+ # remove: once and twice
+ p.remove_subsystem('testnqn')
+ self.assertRaises(nvme.CFSError, p.remove_subsystem, 'testnqn')
+
+ # 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')
+
+ # add again, and try to remove while enabled
+ p.add_subsystem('testnqn')
+ 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_referral(self):
+ root = nvme.Root()
+ root.clear_existing()
+
+ # create port
+ p = nvme.Port(portid=1, mode='create')
+ self.assertEqual(len(list(p.referrals)), 0)
+
+ # create mode
+ r1 = nvme.Referral(p, name="1", mode='create')
+ self.assertIsNotNone(r1)
+ self.assertEqual(len(list(p.referrals)), 1)
+
+ # any mode, should create
+ r2 = nvme.Referral(p, name="2", mode='any')
+ self.assertIsNotNone(r2)
+ self.assertEqual(len(list(p.referrals)), 2)
+
+ # duplicate
+ self.assertRaises(nvme.CFSError, nvme.Referral,
+ p, name="2", mode='create')
+ self.assertEqual(len(list(p.referrals)), 2)
+
+ # lookup using any, should not create
+ r = nvme.Referral(p, name="1", mode='any')
+ self.assertEqual(r1, r)
+ self.assertEqual(len(list(p.referrals)), 2)
+
+ # lookup only
+ r = nvme.Referral(p, name="2", mode='lookup')
+ self.assertEqual(r2, r)
+ self.assertEqual(len(list(p.referrals)), 2)
+
+ # non-existent lookup
+ self.assertRaises(nvme.CFSError, nvme.Referral, p, name="foo",
+ mode='lookup')
+
+ # basic state
+ self.assertTrue('addr' in r.attr_groups)
+ self.assertFalse(r.get_enable())
+
+ # now set trtype to loop and other attrs and enable
+ r.set_attr('addr', 'trtype', 'loop')
+ r.set_attr('addr', 'adrfam', 'ipv4')
+ r.set_attr('addr', 'traddr', '192.168.0.1')
+ r.set_attr('addr', 'treq', 'not required')
+ r.set_attr('addr', 'trsvcid', '1023')
+ r.set_enable(1)
+
+ # test double enable
+ r.set_enable(1)
+
+ # test that we can't write to attrs while enabled
+ self.assertRaises(nvme.CFSError, r.set_attr, 'addr', 'trtype',
+ 'rdma')
+ self.assertRaises(nvme.CFSError, r.set_attr, 'addr', 'adrfam',
+ 'ipv6')
+ self.assertRaises(nvme.CFSError, r.set_attr, 'addr', 'traddr',
+ '10.0.0.1')
+ self.assertRaises(nvme.CFSError, r.set_attr, 'addr', 'treq',
+ 'required')
+ self.assertRaises(nvme.CFSError, r.set_attr, 'addr', 'trsvcid',
+ '21')
+
+ # disable: once and twice
+ r.set_enable(0)
+ r.set_enable(0)
+
+ # check that the attrs haven't been tampered with
+ self.assertEqual(r.get_attr('addr', 'trtype'), 'loop')
+ self.assertEqual(r.get_attr('addr', 'adrfam'), 'ipv4')
+ self.assertEqual(r.get_attr('addr', 'traddr'), '192.168.0.1')
+ self.assertEqual(r.get_attr('addr', 'treq'), 'not required')
+ self.assertEqual(r.get_attr('addr', 'trsvcid'), '1023')
+
+ # enable again, and try to remove while enabled
+ r.set_enable(1)
+ r.delete()
+
+ # remove the other one while disabled:
+ r1.delete()
+ self.assertEqual(len(list(p.referrals)), 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()
+
+ self.assertRaises(nvme.CFSError, nvme.Subsystem,
+ nqn='', mode='create')
+ self.assertRaises(nvme.CFSError, nvme.Subsystem,
+ nqn='/', mode='create')
+
+ for l in [ 257, 512, 1024, 2048 ]:
+ toolong = ''.join(random.choice(string.ascii_lowercase)
+ for i in range(l))
+ self.assertRaises(nvme.CFSError, nvme.Subsystem,
+ nqn=toolong, mode='create')
+
+ discover_nqn = "nqn.2014-08.org.nvmexpress.discovery"
+ self.assertRaises(nvme.CFSError, nvme.Subsystem,
+ nqn=discover_nqn, mode='create')
+
+ self.assertRaises(nvme.CFSError, nvme.Port,
+ portid=1 << 17, mode='create')
+
+ @unittest.skipUnless(test_devices_present(),
+ "Devices %s not available or suitable" % ','.join(
+ NVMET_TEST_DEVICES))
+ def test_save_restore(self):
+ 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', NVMET_TEST_DEVICES[0])
+ n.set_enable(1)
+
+ nguid = n.get_attr('device', 'nguid')
+
+ p = nvme.Port(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.add_subsystem('testnqn')
+
+ # save, clear, and restore
+ root.save_to_file('test.json')
+ root.clear_existing()
+ root.restore_from_file('test.json')
+
+ # additional restores should fai
+ self.assertRaises(nvme.CFSError, root.restore_from_file,
+ 'test.json', False)
+
+ # ... unless forced!
+ 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(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'), NVMET_TEST_DEVICES[0])
+ 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')
+ self.assertIn('testnqn', p.subsystems)
+ self.assertNotIn('testtnqn2', p.subsystems)
diff --git a/nvmetcli b/nvmetcli
new file mode 100755
index 0000000..d949891
--- /dev/null
+++ b/nvmetcli
@@ -0,0 +1,754 @@
+#!/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.
+'''
+
+from __future__ import print_function
+
+import os
+import sys
+import configshell_fb as configshell
+import nvmet as nvme
+import errno
+from string import hexdigits
+import uuid
+
+
+def ngiud_set(nguid):
+ return any(c in hexdigits and c != '0' for c in nguid)
+
+
+class UINode(configshell.node.ConfigNode):
+ def __init__(self, name, parent=None, cfnode=None, shell=None):
+ configshell.node.ConfigNode.__init__(self, name, parent, shell)
+ self.cfnode = cfnode
+ if self.cfnode:
+ if self.cfnode.attr_groups:
+ for group in self.cfnode.attr_groups:
+ self._init_group(group)
+ self.refresh()
+
+ def _init_group(self, group):
+ setattr(self.__class__, "ui_getgroup_%s" % group,
+ lambda self, attr:
+ self.cfnode.get_attr(group, attr))
+ setattr(self.__class__, "ui_setgroup_%s" % group,
+ lambda self, attr, value:
+ self.cfnode.set_attr(group, attr, value))
+
+ 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 refresh(self):
+ self._children = set([])
+
+ def status(self):
+ return "None"
+
+ def ui_command_refresh(self):
+ '''
+ Refreshes and updates the objects tree from the current path.
+ '''
+ self.refresh()
+
+ def ui_command_status(self):
+ '''
+ Displays the current node's status summary.
+
+ SEE ALSO
+ ========
+ B{ls}
+ '''
+ self.shell.log.info("Status for %s: %s" % (self.path, self.status()))
+
+ def ui_command_saveconfig(self, savefile=None):
+ '''
+ Saves the current configuration to a file so that it can be restored
+ on next boot.
+ '''
+ node = self
+ while node.parent is not None:
+ node = node.parent
+ node.cfnode.save_to_file(savefile)
+
+
+class UIRootNode(UINode):
+ def __init__(self, shell):
+ UINode.__init__(self, '/', parent=None, cfnode=nvme.Root(),
+ shell=shell)
+
+ def refresh(self):
+ self._children = set([])
+ UISubsystemsNode(self)
+ UIPortsNode(self)
+ UIHostsNode(self)
+
+ 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(UINode):
+ def __init__(self, parent):
+ UINode.__init__(self, 'subsystems', parent)
+
+ 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 omitted, 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{create}
+ '''
+ subsystem = nvme.Subsystem(nqn, mode='lookup')
+ subsystem.delete()
+ self.refresh()
+
+
+class UISubsystemNode(UINode):
+ ui_desc_attr = {
+ 'allow_any_host': ('string', 'Allow access by any host if set to 1'),
+ 'serial': ('string', 'Export serial number to hosts'),
+ 'version': ('string', 'Export version number to hosts'),
+ }
+
+ def __init__(self, parent, cfnode):
+ UINode.__init__(self, cfnode.nqn, parent, cfnode)
+
+ def refresh(self):
+ self._children = set([])
+ UINamespacesNode(self)
+ UIAllowedHostsNode(self)
+
+ def summary(self):
+ info = []
+ info.append("version=" + self.cfnode.get_attr("attr", "version"))
+ info.append("allow_any=" +
+ self.cfnode.get_attr("attr", "allow_any_host"))
+ info.append("serial=" + self.cfnode.get_attr("attr", "serial"))
+ return (", ".join(info), True)
+
+
+class UINamespacesNode(UINode):
+ def __init__(self, parent):
+ UINode.__init__(self, 'namespaces', parent)
+
+ 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 omitted, 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{create}
+ '''
+ namespace = nvme.Namespace(self.parent.cfnode, nsid, mode='lookup')
+ namespace.delete()
+ self.refresh()
+
+
+class UINamespaceNode(UINode):
+ ui_desc_device = {
+ 'path': ('string', 'Backing device path.'),
+ 'nguid': ('string', 'Namspace Global Unique Identifier.'),
+ 'uuid': ('string', 'Namespace Universally Unique Identifier.'),
+ }
+
+ def __init__(self, parent, cfnode):
+ UINode.__init__(self, str(cfnode.nsid), parent, cfnode)
+
+ def status(self):
+ if self.cfnode.get_enable():
+ return "enabled"
+ return "disabled"
+
+ def ui_command_enable(self):
+ '''
+ Enables the current Namespace.
+
+ SEE ALSO
+ ========
+ B{disable}
+ '''
+ if self.cfnode.get_enable():
+ self.shell.log.info("The Namespace is already enabled.")
+ else:
+ try:
+ self.cfnode.set_enable(1)
+ self.shell.log.info("The Namespace has been enabled.")
+ except Exception as e:
+ raise configshell.ExecutionError(
+ "The Namespace could not be enabled.")
+
+ def ui_command_disable(self):
+ '''
+ Disables the current Namespace.
+
+ SEE ALSO
+ ========
+ B{enable}
+ '''
+ if not self.cfnode.get_enable():
+ self.shell.log.info("The Namespace is already disabled.")
+ else:
+ try:
+ self.cfnode.set_enable(0)
+ self.shell.log.info("The Namespace has been disabled.")
+ except Exception as e:
+ raise configshell.ExecutionError(
+ "The Namespace could not be disabled.")
+
+ def ui_command_grpid(self, grpid):
+ '''
+ Sets the ANA Group ID of the current Namespace to I{grpid}
+ '''
+ try:
+ self.cfnode.set_grpid(grpid)
+ except Exception as e:
+ raise configshell.ExecutionError(
+ "Failed to set ANA Group ID for this Namespace.")
+
+ def summary(self):
+ info = []
+ info.append("path=" + self.cfnode.get_attr("device", "path"))
+ ns_uuid = self.cfnode.get_attr("device", "uuid")
+ if uuid.UUID(ns_uuid).int != 0:
+ info.append("uuid=" + str(ns_uuid))
+ ns_nguid = self.cfnode.get_attr("device", "nguid")
+ if ngiud_set(ns_nguid):
+ info.append("nguid=" + ns_nguid)
+ if self.cfnode.grpid != 0:
+ info.append("grpid=" + str(self.cfnode.grpid))
+ info.append("enabled" if self.cfnode.get_enable() else "disabled")
+ ns_enabled = self.cfnode.get_enable()
+ return (", ".join(info), True if ns_enabled == 1 else ns_enabled)
+
+
+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)
+
+ def refresh(self):
+ self._children = set([])
+ for port in self.parent.cfnode.ports:
+ UIPortNode(self, port)
+
+ def ui_command_create(self, portid=None):
+ '''
+ Creates a new NVMe port with portid I{portid}.
+
+ SEE ALSO
+ ========
+ B{delete}
+ '''
+ port = nvme.Port(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{create}
+ '''
+ port = nvme.Port(portid, mode='lookup')
+ port.delete()
+ self.refresh()
+
+
+class UIPortNode(UINode):
+ ui_desc_addr = {
+ 'adrfam': ('string', 'Address Family (e.g. ipv4 or fc)'),
+ 'treq': ('string', 'Transport Security Requirements'),
+ 'traddr': ('string',
+ 'Transport Address (e.g. IP Address or FC wwnn:wwpn)'),
+ 'trsvcid': ('string', 'Transport Service ID (e.g. IP Port)'),
+ 'trtype': ('string', 'Transport Type (e.g. rdma or loop or fc)'),
+ }
+ ui_desc_param = {
+ 'inline_data_size': ('string', 'Port inline data size in bytes'),
+ }
+
+ def __init__(self, parent, cfnode):
+ UINode.__init__(self, str(cfnode.portid), parent, cfnode)
+ UIPortSubsystemsNode(self)
+ try:
+ next(cfnode.ana_groups)
+ except StopIteration:
+ pass
+ else:
+ UIANAGroupsNode(self)
+ UIReferralsNode(self)
+
+ def summary(self):
+ info = []
+ info.append("trtype=" + self.cfnode.get_attr("addr", "trtype"))
+ info.append("traddr=" + self.cfnode.get_attr("addr", "traddr"))
+ trsvcid = self.cfnode.get_attr("addr", "trsvcid")
+ if trsvcid != "none":
+ info.append("trsvcid=%s" % trsvcid)
+
+ '''
+ Support older target driver w/o the inline_data_size parameter
+ '''
+ try:
+ inline_data_size = self.cfnode.get_attr("param", "inline_data_size")
+ except Exception as e:
+ inline_data_size = "n/a"
+ if inline_data_size != "n/a":
+ info.append("inline_data_size=" + inline_data_size)
+ enabled = self.cfnode.subsystems or list(self.cfnode.referrals)
+ return (", ".join(info), True if enabled else 0)
+
+
+class UIPortSubsystemsNode(UINode):
+ def __init__(self, parent):
+ UINode.__init__(self, 'subsystems', parent)
+
+ def refresh(self):
+ self._children = set([])
+ for host in self.parent.cfnode.subsystems:
+ UIPortSubsystemNode(self, host)
+
+ def ui_command_create(self, nqn):
+ '''
+ Grants access to the subsystem specified by I{nqn} through the
+ parent port.
+
+ SEE ALSO
+ ========
+ B{delete}
+ '''
+ self.parent.cfnode.add_subsystem(nqn)
+ UIPortSubsystemNode(self, nqn)
+
+ def ui_complete_create(self, parameters, text, current_param):
+ completions = []
+ if current_param == 'nqn':
+ for subsys in self.get_node('/subsystems').children:
+ completions.append(subsys.cfnode.nqn)
+
+ if len(completions) == 1:
+ return [completions[0] + ' ']
+ else:
+ return completions
+
+ def ui_command_delete(self, nqn):
+ '''
+ Removes access to the subsystem specified by I{nqn} through the
+ parent port.
+
+ SEE ALSO
+ ========
+ B{create}
+ '''
+ self.parent.cfnode.remove_subsystem(nqn)
+ self.refresh()
+
+ def ui_complete_delete(self, parameters, text, current_param):
+ completions = []
+ if current_param == 'nqn':
+ for nqn in self.parent.cfnode.subsystems:
+ completions.append(nqn)
+
+ if len(completions) == 1:
+ return [completions[0] + ' ']
+ else:
+ return completions
+
+
+class UIPortSubsystemNode(UINode):
+ def __init__(self, parent, nqn):
+ UINode.__init__(self, nqn, parent)
+
+
+class UIReferralsNode(UINode):
+ def __init__(self, parent):
+ UINode.__init__(self, 'referrals', parent)
+
+ def refresh(self):
+ self._children = set([])
+ for r in self.parent.cfnode.referrals:
+ UIReferralNode(self, r)
+
+ def ui_command_create(self, name):
+ '''
+ Creates a new referral.
+
+ SEE ALSO
+ ========
+ B{delete}
+ '''
+ r = nvme.Referral(self.parent.cfnode, name, mode='create')
+ UIReferralNode(self, r)
+
+ def ui_command_delete(self, name):
+ '''
+ Deletes the referral with the specified I{name}.
+
+ SEE ALSO
+ ========
+ B{create}
+ '''
+ r = nvme.Referral(self.parent.cfnode, name, mode='lookup')
+ r.delete()
+ self.refresh()
+
+
+class UIReferralNode(UINode):
+ ui_desc_addr = {
+ 'adrfam': ('string', 'Address Family (e.g. ipv4 or fc)'),
+ 'treq': ('string', 'Transport Security Requirements'),
+ 'traddr': ('string',
+ 'Transport Address (e.g. IP Address or FC wwnn:wwpn)'),
+ 'trsvcid': ('string', 'Transport Service ID (e.g. IP Port)'),
+ 'trtype': ('string', 'Transport Type (e.g. rdma or loop or fc)'),
+ 'portid': ('number', 'Port identifier'),
+ }
+
+ def __init__(self, parent, cfnode):
+ UINode.__init__(self, cfnode.name, parent, cfnode)
+
+ def status(self):
+ if self.cfnode.get_enable():
+ return "enabled"
+ return "disabled"
+
+ def ui_command_enable(self):
+ '''
+ Enables the current Referral.
+
+ SEE ALSO
+ ========
+ B{disable}
+ '''
+ if self.cfnode.get_enable():
+ self.shell.log.info("The Referral is already enabled.")
+ else:
+ try:
+ self.cfnode.set_enable(1)
+ self.shell.log.info("The Referral has been enabled.")
+ except Exception as e:
+ raise configshell.ExecutionError(
+ "The Referral could not be enabled.")
+
+ def ui_command_disable(self):
+ '''
+ Disables the current Referral.
+
+ SEE ALSO
+ ========
+ B{enable}
+ '''
+ if not self.cfnode.get_enable():
+ self.shell.log.info("The Referral is already disabled.")
+ else:
+ try:
+ self.cfnode.set_enable(0)
+ self.shell.log.info("The Referral has been disabled.")
+ except Exception as e:
+ raise configshell.ExecutionError(
+ "The Referral could not be disabled.")
+
+
+class UIANAGroupsNode(UINode):
+ def __init__(self, parent):
+ UINode.__init__(self, 'ana_groups', parent)
+
+ def refresh(self):
+ self._children = set([])
+ for a in self.parent.cfnode.ana_groups:
+ UIANAGroupNode(self, a)
+
+ def ui_command_create(self, grpid):
+ '''
+ Creates a new ANA Group.
+
+ SEE ALSO
+ ========
+ B{delete}
+ '''
+ a = nvme.ANAGroup(self.parent.cfnode, grpid, mode='create')
+ UIANAGroupNode(self, a)
+
+ def ui_command_delete(self, grpid):
+ '''
+ Deletes the ANA Group with the specified I{name}.
+
+ SEE ALSO
+ ========
+ B{create}
+ '''
+ a = nvme.ANAGroup(self.parent.cfnode, grpid, mode='lookup')
+ a.delete()
+ self.refresh()
+
+
+class UIANAGroupNode(UINode):
+ ui_desc_ana = {
+ 'state' : ('string', 'ANA state'),
+ }
+
+ def __init__(self, parent, cfnode):
+ UINode.__init__(self, str(cfnode.grpid), parent, cfnode)
+
+ def summary(self):
+ info = []
+ info.append("state=" + self.cfnode.get_attr("ana", "state"))
+ return (", ".join(info), True)
+
+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])
+ print(" %s clear" % sys.argv[0])
+ print(" %s ls" % sys.argv[0])
+ sys.exit(-1)
+
+
+def save(to_file):
+ nvme.Root().save_to_file(to_file)
+
+
+def restore(from_file):
+ errors = None
+
+ try:
+ errors = nvme.Root().restore_from_file(from_file)
+ except IOError as e:
+ if not from_file:
+ from_file = nvme.DEFAULT_SAVE_FILE
+
+ if e.errno == errno.ENOENT:
+ # Not an error if the restore file is not present
+ print("No saved config file at %s, ok, exiting" % from_file)
+ sys.exit(0)
+ else:
+ print("Error processing config file at %s, error %s, exiting" %
+ (from_file, str(e)))
+ sys.exit(1)
+
+ # These errors are non-fatal
+ for error in errors:
+ print(error)
+
+ sys.exit(0)
+
+
+def clear(unused):
+ nvme.Root().clear_existing()
+
+
+def ls(unused):
+ shell = configshell.shell.ConfigShell('~/.nvmetcli')
+ UIRootNode(shell)
+ shell.run_cmdline("ls")
+ sys.exit(0)
+
+
+funcs = dict(save=save, restore=restore, clear=clear, ls=ls)
+
+
+def main():
+ if os.geteuid() != 0:
+ print("%s: must run as root." % sys.argv[0], file=sys.stderr)
+ sys.exit(-1)
+
+ 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
+
+ try:
+ shell = configshell.shell.ConfigShell('~/.nvmetcli')
+ UIRootNode(shell)
+ except Exception as msg:
+ shell.log.error(str(msg))
+ return
+
+ while not shell._exit:
+ try:
+ shell.run_interactive()
+ except Exception as msg:
+ shell.log.error(str(msg))
+
+
+if __name__ == "__main__":
+ main()
diff --git a/rdma.json b/rdma.json
new file mode 100644
index 0000000..6fc326c
--- /dev/null
+++ b/rdma.json
@@ -0,0 +1,42 @@
+{
+ "hosts": [
+ {
+ "nqn": "hostnqn"
+ }
+ ],
+ "ports": [
+ {
+ "addr": {
+ "adrfam": "ipv4",
+ "traddr": "192.168.6.68",
+ "treq": "not specified",
+ "trsvcid": "4420",
+ "trtype": "rdma"
+ },
+ "portid": 2,
+ "referrals": [],
+ "subsystems": [
+ "testnqn"
+ ]
+ }
+ ],
+ "subsystems": [
+ {
+ "allowed_hosts": [],
+ "attr": {
+ "allow_any_host": "1"
+ },
+ "namespaces": [
+ {
+ "device": {
+ "nguid": "ef90689c-6c46-d44c-89c1-4067801309a8",
+ "path": "/dev/nvme0n1"
+ },
+ "enable": 1,
+ "nsid": 1
+ }
+ ],
+ "nqn": "testnqn"
+ }
+ ]
+}
diff --git a/rpm/nvmetcli.spec.tmpl b/rpm/nvmetcli.spec.tmpl
new file mode 100644
index 0000000..f1b5533
--- /dev/null
+++ b/rpm/nvmetcli.spec.tmpl
@@ -0,0 +1,55 @@
+Name: nvmetcli
+License: Apache License 2.0
+Group: Applications/System
+Summary: Command line interface for the kernel NVMe nvmet
+Version: VERSION
+Release: 1%{?dist}
+URL: http://git.infradead.org/users/hch/nvmetcli.git
+Source: nvmetcli-%{version}.tar.gz
+BuildRoot: %{_tmppath}/%{name}-%{version}-%{release}-rpmroot
+BuildArch: noarch
+BuildRequires: python-devel python-setuptools systemd-units
+Requires: python-configshell python-kmod python-six
+Requires(post): systemd
+Requires(preun): systemd
+Requires(postun): systemd
+
+%description
+This package contains the command line interface to the NVMe over Fabrics
+nvmet in the Linux kernel. It allows configuring the nvmet interactively
+as well as saving / restoring the configuration to / from a json file.
+
+%prep
+%setup -q -n nvmetcli-%{version}
+
+%build
+%{__python} setup.py build
+
+%install
+rm -rf %{buildroot}
+%{__python} setup.py install --skip-build --root=%{buildroot} --prefix=usr
+mkdir -p %{buildroot}%{_sysconfdir}/nvmet
+mkdir -p %{buildroot}%{_unitdir}
+install -m 644 nvmet.service %{buildroot}%{_unitdir}/nvmet.service
+
+%clean
+rm -rf %{buildroot}
+
+%post
+%systemd_post nvmet.service
+
+%preun
+%systemd_preun nvmet.service
+
+%postun
+%systemd_postun_with_restart nvmet.service
+
+%files
+%defattr(-,root,root,-)
+%{python_sitelib}
+%dir %{_sysconfdir}/nvmet
+/usr/sbin/nvmetcli
+%{_unitdir}/nvmet.service
+%doc COPYING README
+
+%changelog
diff --git a/setup.cfg b/setup.cfg
new file mode 100644
index 0000000..ed3bf6e
--- /dev/null
+++ b/setup.cfg
@@ -0,0 +1,2 @@
+[install]
+install_scripts=/usr/sbin
diff --git a/setup.py b/setup.py
new file mode 100755
index 0000000..2b202e8
--- /dev/null
+++ b/setup.py
@@ -0,0 +1,31 @@
+#! /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.8,
+ description = 'NVMe target configuration tool',
+ license = 'Apache 2.0',
+ maintainer = 'Christoph Hellwig',
+ maintainer_email = 'hch@lst.de',
+ test_suite='nose2.collector.collector',
+ packages = ['nvmet'],
+ scripts=['nvmetcli']
+ )
diff --git a/tcp.json b/tcp.json
new file mode 100644
index 0000000..e6ff029
--- /dev/null
+++ b/tcp.json
@@ -0,0 +1,58 @@
+{
+ "hosts": [],
+ "ports": [
+ {
+ "addr": {
+ "adrfam": "ipv4",
+ "traddr": "192.168.122.33",
+ "treq": "not specified",
+ "trsvcid": "4420",
+ "trtype": "tcp"
+ },
+ "ana_groups": [
+ {
+ "ana": {
+ "state": "optimized"
+ },
+ "grpid": 1
+ }
+ ],
+ "param": {
+ "inline_data_size": "16384",
+ "pi_enable": "0"
+ },
+ "portid": 1,
+ "referrals": [],
+ "subsystems": [
+ "nvmet-always"
+ ]
+ }
+ ],
+ "subsystems": [
+ {
+ "allowed_hosts": [],
+ "attr": {
+ "allow_any_host": "1",
+ "cntlid_max": "65519",
+ "cntlid_min": "1",
+ "model": "Linux",
+ "pi_enable": "0",
+ "serial": "123456789abcdef",
+ "version": "1.3"
+ },
+ "namespaces": [
+ {
+ "ana_grpid": 1,
+ "device": {
+ "nguid": "00000000-0000-0000-0000-000000000000",
+ "path": "/dev/nvme0n1",
+ "uuid": "d592cdf3-5d1c-44e0-8412-3fcf7d99df27"
+ },
+ "enable": 1,
+ "nsid": 1
+ }
+ ],
+ "nqn": "nvmet-always"
+ }
+ ]
+}