├── COPYING ├── Makefile ├── README.md ├── doc └── targetcli.8 ├── rpm └── targetcli.spec.tmpl ├── scripts ├── target.init ├── targetcli └── targetcli-ng ├── setup.py └── targetcli ├── __init__.py ├── cli.py ├── cli_config.py ├── cli_live.py ├── cli_logger.py ├── ui_backstore.py ├── ui_backstore_legacy.py ├── ui_node.py ├── ui_root.py └── ui_target.py /COPYING: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # This file is part of LIO(tm). 2 | # Copyright (c) 2011-2014 by Datera, Inc 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); you may 5 | # not use this file except in compliance with the License. You may obtain 6 | # a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | # License for the specific language governing permissions and limitations 14 | # under the License. 15 | # 16 | 17 | NAME = targetcli 18 | GIT_BRANCH = $$(git branch | grep \* | tr -d \*) 19 | VERSION = $$(basename $$(git describe --tags | grep -o '[0-9].*$$') | sed 's/\(.*-[0-9]*\)-\([a-z0-9]*\)/\1~\2/g' | tr - .) 20 | 21 | .PHONY : all version clean cleanall release deb debinstall rpm 22 | 23 | all: 24 | @echo "Usage:" 25 | @echo 26 | @echo " make deb - Builds debian packages." 27 | @echo " make debinstall - Builds and installs debian packages." 28 | @echo " (requires sudo access)" 29 | @echo " make rpm - Builds rpm packages." 30 | @echo " make release - Generates the release tarball." 31 | @echo 32 | @echo " make clean - Cleanup the local repository build files." 33 | @echo " make cleanall - Also remove dist/*" 34 | 35 | version: 36 | @echo $(VERSION) 37 | 38 | clean: 39 | @rm -frv ${NAME}/*.html 40 | @rm -frv ${NAME}.egg-info MANIFEST build 41 | @rm -frv debian/tmp 42 | @rm -fv build-stamp 43 | @rm -fv dpkg-buildpackage.log dpkg-buildpackage.version 44 | @rm -frv *.rpm 45 | @rm -fv debian/files debian/*.log debian/*.substvars 46 | @rm -frv debian/${NAME}-doc/ debian/python2.5-${NAME}/ 47 | @rm -frv debian/python2.6-${NAME}/ debian/python-${NAME}/ 48 | @rm -frv results 49 | @rm -fv rpm/*.spec *.spec rpm/sed* sed* 50 | @rm -frv ${NAME}-* 51 | @rm -frv *.rpm warn${NAME}.txt build${NAME} 52 | @rm -fv debian/*.debhelper.log debian/*.debhelper debian/*.substvars debian/files 53 | @rm -fvr debian/${NAME}-frozen/ debian/${NAME}-python2.5/ 54 | @rm -fvr debian/${NAME}-python2.6/ debian/${NAME}/ debian/${NAME}-doc/ 55 | @rm -frv log/ 56 | @find . -name *.swp -exec rm -v {} \; 57 | @find . -name *.pyc -exec rm -vf {} \; 58 | @find . -name *~ -exec rm -v {} \; 59 | @find . -name \#*\# -exec rm -v {} \; 60 | @echo "Finished cleanup." 61 | 62 | cleanall: clean 63 | @rm -frv dist 64 | 65 | release: build/release-stamp 66 | build/release-stamp: 67 | @mkdir -p build 68 | @echo "Exporting the repository files..." 69 | @git archive ${GIT_BRANCH} --prefix ${NAME}-${VERSION}/ \ 70 | | (cd build; tar xfp -) 71 | @cp -pr debian/ build/${NAME}-${VERSION} 72 | @echo "Cleaning up the target tree..." 73 | @rm -f build/${NAME}-${VERSION}/Makefile 74 | @rm -f build/${NAME}-${VERSION}/.gitignore 75 | @rm -rf build/${NAME}-${VERSION}/bin 76 | @echo "Fixing version string..." 77 | @sed -i "s/__version__ = .*/__version__ = '${VERSION}'/g" \ 78 | build/${NAME}-${VERSION}/${NAME}/__init__.py 79 | @echo "Generating rpm specfile from template..." 80 | @cd build/${NAME}-${VERSION}; \ 81 | for spectmpl in rpm/*.spec.tmpl; do \ 82 | sed -i "s/Version:\( *\).*/Version:\1${VERSION}/g" $${spectmpl}; \ 83 | mv $${spectmpl} $$(basename $${spectmpl} .tmpl); \ 84 | done; \ 85 | rmdir rpm 86 | @echo "Generating rpm changelog..." 87 | @( \ 88 | version=$(VERSION); \ 89 | author=$$(git show HEAD --format="format:%an <%ae>" -s); \ 90 | date=$$(git show HEAD --format="format:%ad" -s \ 91 | | awk '{print $$1,$$2,$$3,$$5}'); \ 92 | hash=$$(git show HEAD --format="format:%H" -s); \ 93 | echo '* '"$${date} $${author} $${version}-1"; \ 94 | echo " - Generated from git commit $${hash}."; \ 95 | ) >> $$(ls build/${NAME}-${VERSION}/*.spec) 96 | @echo "Generating debian changelog..." 97 | @( \ 98 | version=$(VERSION); \ 99 | author=$$(git show HEAD --format="format:%an <%ae>" -s); \ 100 | date=$$(git show HEAD --format="format:%aD" -s); \ 101 | day=$$(git show HEAD --format='format:%ai' -s \ 102 | | awk '{print $$1}' \ 103 | | awk -F '-' '{print $$3}' | sed 's/^0/ /g'); \ 104 | date=$$(echo $${date} \ 105 | | awk '{print $$1, "'"$${day}"'", $$3, $$4, $$5, $$6}'); \ 106 | hash=$$(git show HEAD --format="format:%H" -s); \ 107 | echo "${NAME} ($${version}) unstable; urgency=low"; \ 108 | echo; \ 109 | echo " * Generated from git commit $${hash}."; \ 110 | echo; \ 111 | echo " -- $${author} $${date}"; \ 112 | echo; \ 113 | ) > build/${NAME}-${VERSION}/debian/changelog 114 | @find build/${NAME}-${VERSION}/ -exec \ 115 | touch -t $$(date -d @$$(git show -s --format="format:%at") \ 116 | +"%Y%m%d%H%M.%S") {} \; 117 | @mkdir -p dist 118 | @cd build; tar -c --owner=0 --group=0 --numeric-owner \ 119 | --format=gnu -b20 --quoting-style=escape \ 120 | -f ../dist/${NAME}-${VERSION}.tar \ 121 | $$(find ${NAME}-${VERSION} -type f | sort) 122 | @gzip -6 -n dist/${NAME}-${VERSION}.tar 123 | @echo "Generated release tarball:" 124 | @echo " $$(ls dist/${NAME}-${VERSION}.tar.gz)" 125 | @touch build/release-stamp 126 | 127 | deb: release build/deb-stamp 128 | build/deb-stamp: 129 | @echo "Building debian packages..." 130 | @cd build/${NAME}-${VERSION}; \ 131 | dpkg-buildpackage -rfakeroot -us -uc 132 | @mv build/*_${VERSION}_*.deb dist/ 133 | @echo "Generated debian packages:" 134 | @for pkg in $$(ls dist/*_${VERSION}_*.deb); do echo " $${pkg}"; done 135 | @touch build/deb-stamp 136 | 137 | debinstall: deb 138 | @echo "Installing $$(ls dist/*_${VERSION}_*.deb)" 139 | @sudo dpkg -i $$(ls dist/*_${VERSION}_*.deb) 140 | 141 | rpm: release build/rpm-stamp 142 | build/rpm-stamp: 143 | @echo "Building rpm packages..." 144 | @mkdir -p build/rpm 145 | @build=$$(pwd)/build/rpm; dist=$$(pwd)/dist/; rpmbuild \ 146 | --define "_topdir $${build}" --define "_sourcedir $${dist}" \ 147 | --define "_rpmdir $${build}" --define "_buildir $${build}" \ 148 | --define "_srcrpmdir $${build}" -ba build/${NAME}-${VERSION}/*.spec 149 | @mv build/rpm/*-${VERSION}*.src.rpm dist/ 150 | @mv build/rpm/*/*-${VERSION}*.rpm dist/ 151 | @echo "Generated rpm packages:" 152 | @for pkg in $$(ls dist/*-${VERSION}*.rpm); do echo " $${pkg}"; done 153 | @touch build/rpm-stamp 154 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # TargetCLI 2 | 3 | TargetCLI is the LIO commmand-line administration tool for managing the Linux 4 | SCSI Target, and its third-party target fabric modules and backend storage 5 | objects. 6 | 7 | Based on RTSLib, it allows direct manipulation of all SCSI Target objects like 8 | storage objects, SCSI targets, TPGs, LUNs and ACLs, as well as manage startup 9 | system configuration for the SCSI Target subsystem. 10 | 11 | TargetCLI can be used either as a regular CLI tool, one command at a time, or 12 | as an interactive shell based on the python configshell CLI framework, with 13 | full auto-complete support and inline documentation. 14 | 15 | TargetCLI is part of the Linux Kernel's SCSI Target's userspace management 16 | tools. 17 | 18 | ## Installation 19 | 20 | TargetCLI is currently part of several Linux distributions. In most cases, 21 | simply installing the version packaged by your favorite Linux distribution is 22 | the best way to get it running. 23 | 24 | ## Migrating away from a targetcli < 3.0 setup 25 | 26 | Prior to version 3.x, TargetCLI relied on lio-utils for managing the target's 27 | startup configuration. Unfortunately, rtslib.Config - now used by targetcli and 28 | the `/etc/init.d/target` initscript for startup config save and restore 29 | operations - is incompatible with the legacy lio-utils config files. 30 | 31 | However, the new initscript has a special provision for this case. 32 | When attempting to start the target service when there is no 33 | `/etc/target/scsi_target.lio` configuration file present, a check is made to see 34 | if there is a target configuration currently running on the system. If there is, 35 | it is assumed to be a keeper, and the initscript will attempt to dump it to the 36 | system startup configuration file `/etc/target/scsi_target.lio`. 37 | 38 | When migrating from a lio-utils install, the trick is to prevent the old lio-utils 39 | package removal from stopping the service. For this, you can simply empty the 40 | lio-utils version of `/etc/init.d/target` - or the equivalent location for your 41 | Linux distribution. 42 | 43 | Example on Debian: 44 | 45 | echo > /etc/init.d/target 46 | dpkg --purge lio-utils 47 | apt-get install targetcli 48 | 49 | ## Building from source 50 | 51 | The packages are very easy to build and install from source as long as 52 | you're familiar with your Linux Distribution's package manager: 53 | 54 | 1. Clone the github repository for TargetCLI using `git clone 55 | https://github.com/Datera/targetcli.git`. 56 | 57 | 2. Make sure build dependencies are installed. To build TargetCLI, you will need: 58 | 59 | * GNU Make. 60 | * python 2.6 or 2.7 61 | * A few python libraries: rtslib, configshell, lio-utils 62 | * Your favorite distribution's package developement tools, like rpm for 63 | Redhat-based systems or dpkg-dev and debhelper for Debian systems. 64 | 65 | 3. From the cloned git repository, run `make deb` to generate a Debian 66 | package, or `make rpm` for a Redhat package. 67 | 68 | 4. The newly built packages will be generated in the `dist/` directory. 69 | 70 | 5. To cleanup the repository, use `make clean` or `make cleanall` which also 71 | removes `dist/*` files. 72 | 73 | ## Documentation 74 | 75 | A manpage is provided with this packages, simply use `man targetcli` to get 76 | more information. 77 | 78 | An other good source of information is the http://linux-iscsi.org wiki, 79 | offering many resources such as a the TargetCLI User's Guide, online at 80 | http://linux-iscsi.org/wiki/targetcli. 81 | 82 | ## Mailing-list 83 | 84 | All contributions, suggestions and bugfixes are welcome! 85 | 86 | To report a bug, submit a patch or simply stay up-to-date on the Linux SCSI 87 | Target developments, you can subscribe to the Linux Kernel SCSI Target 88 | development mailing-list by sending an email message containing only 89 | `subscribe target-devel` to 90 | 91 | The archives of this mailing-list can be found online at 92 | http://dir.gmane.org/gmane.linux.scsi.target.devel 93 | 94 | ## Author 95 | 96 | LIO was developed by Datera, Inc. 97 | http://www.datera.io 98 | 99 | The original author and current maintainer is 100 | Jerome Martin 101 | -------------------------------------------------------------------------------- /doc/targetcli.8: -------------------------------------------------------------------------------- 1 | .TH targetcli 8 2 | .SH NAME 3 | .B targetcli 4 | .SH DESCRIPTION 5 | .B targetcli 6 | is a shell for viewing, editing, and saving the configuration of 7 | the kernel's target subsystem, also known as TCM/LIO. It enables the 8 | administrator to assign local storage resources backed by either files, 9 | volumes, local SCSI devices, or ramdisk, and export them to remote systems via 10 | network fabrics, such as iSCSI or FCoE. 11 | .P 12 | The configuration layout is tree-based, similar to a filesystem, and 13 | navigated in a similar manner. 14 | .SH USAGE 15 | .B targetcli 16 | .P 17 | .B targetcli [cmd] 18 | .P 19 | Invoke 20 | .B targetcli 21 | as root to enter the configuration shell, or 22 | follow with a command to execute but do not enter the shell. Use 23 | .B ls 24 | to list nodes below the current path. 25 | Moving 26 | around the tree is accomplished by the 27 | .B cd 28 | command, or by entering 29 | the new location directly. Objects are created using 30 | .BR create , 31 | removed using 32 | .BR delete . 33 | Use 34 | .B "help " 35 | for additional usage 36 | information. Tab-completion is available for commands and command 37 | arguments. 38 | .P 39 | Configuration changes in 40 | targetcli are made immediately to the underlying kernel target 41 | configuration. Settings will not be retained across reboot unless 42 | .B saveconfig 43 | is either explicitly called, or implicitly by exiting the shell with 44 | the global preference 45 | .B auto_save_on_exit 46 | set to 47 | .BR true , 48 | the default. 49 | .P 50 | .SH EXAMPLES 51 | To export a storage resource, 1) define a storage object using 52 | a backstore, then 2) export the object via a network fabric, such as 53 | iSCSI or FCoE. 54 | .SS DEFINING A STORAGE OBJECT WITHIN A BACKSTORE 55 | .B backstores/fileio create disk1 /disks/disk1.img 140M 56 | .br 57 | Creates a storage object named 58 | .I disk1 59 | with the given path and size. 60 | .B targetcli 61 | supports common size abbreviations like 'M', 'G', and 'T'. 62 | .P 63 | In addition to the 64 | .I fileio 65 | backstore for file-backed volumes, other backstore types include 66 | .I iblock 67 | for block-device-backed volumes, and 68 | .I pscsi 69 | for volumes backed by local SCSI devices. 70 | .I rd_mcp 71 | backstore creates ram-based storage objects. See the built-in help 72 | for more details on the required parameters for each backstore type. 73 | .SS EXPORTING A STORAGE OBJECT VIA FCOE 74 | .B tcm_fc/ create 20:00:00:19:99:a8:34:bc 75 | .br 76 | Create an FCoE target with the given WWN. 77 | .B targetcli 78 | can tab-complete the WWN based on registered FCoE interfaces. If none 79 | are found, verify that they are properly configured and are shown in 80 | the output of 81 | .BR "fcoeadm -i" . 82 | .P 83 | .B tcm_fc/20:00:00:19:99:a8:34:bc/ 84 | .br 85 | If 86 | .B auto_cd_after_create 87 | is set to false, change to the configuration node for the given 88 | target, equivalent to giving the command prefixed by 89 | .BR cd . 90 | .P 91 | .B luns/ create /backstores/fileio/disk1 92 | .br 93 | Create a new LUN for the interface, attached to a previously defined 94 | storage object. The storage object now shows up under the /backstores 95 | configuration node as 96 | .BR activated . 97 | .P 98 | .B acls/ create 00:99:88:77:66:55:44:33 99 | .br 100 | Create an ACL (access control list), for defining the resources each 101 | initiator may access. The default behavior is to auto-map existing 102 | LUNs to the ACL; see help for more information. 103 | .P 104 | The LUN should now be accessible via FCoE. 105 | .SS EXPORTING A STORAGE OBJECT VIA ISCSI 106 | .B iscsi/ create 107 | .br 108 | Creates an iSCSI target with a default WWN. It will also create an 109 | initial target portal group called 110 | .IR tpg1 . 111 | .P 112 | .B iqn.2003-01.org.linux-iscsi.test2.x8664:sn123456789012/tpg1/ 113 | .br 114 | An example of changing to the configuration node for the given 115 | target's first target portal group (TPG). This is equivalent to giving 116 | the command prefixed by "cd". (Although more can be useful for certain 117 | setups, most configurations have a single TPG per target. In this 118 | case, configuring the TPG is equivalent to configuring the overall 119 | target.) 120 | .P 121 | .B portals/ create 122 | .br 123 | Add a portal, i.e. an IP address and TCP port via which the target can be 124 | contacted by initiators. Sane defaults are used if these are not 125 | specified. 126 | .P 127 | .B luns/ create /backstores/fileio/disk1 128 | .br 129 | Create a new LUN in the TPG, attached to the storage object that has 130 | previously been defined. The storage object now shows up under the 131 | /backstores configuration node as activated. 132 | .P 133 | .B acls/ create iqn.1994-05.com.redhat:4321576890 134 | .br 135 | Creates an ACL (access control list) for the given iSCSI initiator. 136 | .P 137 | .B acls/iqn.1994-05.com.redhat:4321576890 create 2 0 138 | .br 139 | Gives the initiator access to the first exported LUN (lun0), which the 140 | initiator will see as lun2. The default is to give the initiator 141 | read/write access; if read-only access was desired, an additional "1" 142 | argument would be added to enable write-protect. (Note: if global 143 | setting 144 | .B auto_add_mapped_luns 145 | is true, this step is not necessary.) 146 | .P 147 | .B acls/iqn.1994-05.com.redhat:4321576890 set authentication=0 148 | .br 149 | Purely for example, make the LUNs in the ACL accessible without 150 | authentication. See below for more information on configuring authentication. 151 | .SH OTHER COMMANDS 152 | .B saveconfig 153 | .br 154 | Save the current configuration settings to a file, from which 155 | settings will be restored if the system is rebooted. 156 | .P 157 | This command must be executed from the configuration root node. 158 | .P 159 | .B clearconfig 160 | .br 161 | Clears the entire current local configuration. The parameter 162 | .I confirm=true 163 | must also be given, as a precaution. 164 | .P 165 | This command is executed from the configuration root node. 166 | .P 167 | .B exit 168 | .br 169 | Leave the configuration shell. 170 | .SH SETTINGS GROUPS 171 | Settings are broken into groups. Individual settings are accessed by 172 | .B "get " 173 | and 174 | .BR "set =" , 175 | and the settings of an entire group may be displayed by 176 | .BR "get " . 177 | All except for 178 | .I global 179 | are associated with a particular configuration node. 180 | .SS GLOBAL 181 | Shell-related user-specific settings are in 182 | .IR global , 183 | and are visible from all configuration nodes. They are mostly shell 184 | display options, but some starting with 185 | .B auto_ 186 | affect shell behavior and may merit customization. Global settings 187 | are saved to ~/.targetcli/ upon exit, unlike other groups. 188 | .SS BACKSTORE-SPECIFIC 189 | .B attribute 190 | .br 191 | /backstore// configuration node. Contains values relating 192 | to the backstore and storage object. 193 | .P 194 | .SS ISCSI-SPECIFIC 195 | .B discovery_auth 196 | .br 197 | /iscsi configuration node. Set the normal and mutual authentication 198 | userid and password for discovery sessions, as well as enabling or 199 | disabling it. By default it is disabled -- no authentication is 200 | required for discovery. 201 | .P 202 | .B parameter 203 | .br 204 | /iscsi//tpgX configuration node. ISCSI-specific parameters such as 205 | .IR AuthMethod , 206 | .IR MaxBurstLength , 207 | .IR IFMarker , 208 | .IR DataDigest , 209 | and similar. 210 | .P 211 | .B attribute 212 | .br 213 | /iscsi//tpgX configuration node. Contains implementation-specific 214 | settings for the TPG, such as 215 | .BR authentication , 216 | to enforce or disable authentication for the full-feature phase 217 | (i.e. non-discovery). 218 | .P 219 | .B auth 220 | .br 221 | /iscsi//tpgX/acls/ configuration node. Set the 222 | userid and password for full-feature phase for this ACL. 223 | .SH FILES 224 | .B /etc/target/* 225 | .br 226 | .B /var/lib/target/* 227 | .SH AUTHOR 228 | Written by Jerome Martin . 229 | .br 230 | Man page written by Andy Grover . 231 | .SH REPORTING BUGS 232 | Report bugs to 233 | -------------------------------------------------------------------------------- /rpm/targetcli.spec.tmpl: -------------------------------------------------------------------------------- 1 | %define oname targetcli 2 | 3 | Name: targetcli 4 | License: Apache License 2.0 5 | Group: Applications/System 6 | Summary: RisingTide Systems generic SCSI target CLI shell. 7 | Version: VERSION 8 | Release: 1%{?dist} 9 | URL: http://www.risingtidesystems.com/git/ 10 | Source: %{oname}-%{version}.tar.gz 11 | BuildRoot: %{_tmppath}/%{name}-%{version}-%{release}-rpmroot 12 | BuildArch: noarch 13 | BuildRequires: python-devel, python-rtslib, python-configshell, python-prettytable 14 | Requires: python-rtslib, python-configshell, python-prettytable 15 | Conflicts: targetcli-frozen, rtsadmin-frozen, rtsadmin, lio-utils 16 | Vendor: Datera, Inc. 17 | 18 | %description 19 | RisingTide Systems generic SCSI target CLI shell. 20 | 21 | %prep 22 | %setup -q -n %{oname}-%{version} 23 | 24 | %build 25 | %{__python} setup.py build 26 | 27 | %install 28 | rm -rf %{buildroot} 29 | %{__python} setup.py install --skip-build --root=%{buildroot} --prefix=usr 30 | mkdir -p %{buildroot}/etc/target 31 | mkdir -p %{buildroot}/var/target/pr 32 | mkdir -p %{buildroot}/var/target/alua 33 | mkdir -p %{buildroot}/etc/init.d/ 34 | mkdir -p %{buildroot}/%{_mandir}/man8 35 | cp doc/targetcli.8 %{buildroot}/%{_mandir}/man8 36 | cp scripts/target.init %{buildroot}/etc/init.d/target 37 | 38 | %clean 39 | rm -rf %{buildroot} 40 | 41 | %files 42 | %defattr(-,root,root,-) 43 | %{python_sitelib} 44 | /etc/target 45 | /var/target 46 | /etc/init.d/target 47 | %{_bindir}/targetcli 48 | %{_bindir}/targetcli-ng 49 | %{_mandir}/man8/* 50 | %doc COPYING README.md 51 | 52 | %changelog 53 | -------------------------------------------------------------------------------- /scripts/target.init: -------------------------------------------------------------------------------- 1 | #! /bin/sh 2 | ### BEGIN INIT INFO 3 | # Provides: target 4 | # Required-Start: $network $remote_fs $syslog 5 | # Required-Stop: $network $remote_fs $syslog 6 | # Default-Start: 2 3 4 5 7 | # Default-Stop: 0 1 6 8 | # Short-Description: The Linux SCSI Target service 9 | ### END INIT INFO 10 | 11 | # PATH should only include /usr/* if it runs after the mountnfs.sh script 12 | PATH=/sbin:/usr/sbin:/bin:/usr/bin 13 | DESC="The Linux SCSI Target" 14 | NAME=target 15 | DAEMON=/usr/bin/targetcli 16 | DAEMON_ARGS="" 17 | SCRIPTNAME=/etc/init.d/$NAME 18 | 19 | CFS_BASE="/sys/kernel/config" 20 | CFS_TGT="${CFS_BASE}/target" 21 | CORE_MODS="target_core_mod target_core_pscsi target_core_iblock target_core_file" 22 | STARTUP_CONFIG="/etc/target/scsi_target.lio" 23 | 24 | # Read configuration variable file if it is present 25 | [ -r /etc/default/$NAME ] && . /etc/default/$NAME 26 | 27 | # How we log messages depends on the system 28 | if [ -r /lib/lsb/init-functions ]; then 29 | # LSB systems like Debian 30 | . /lib/lsb/init-functions 31 | elif [ -r /etc/init.d/functions ]; then 32 | # RHEL without optional redhat-lsb 33 | . /etc/init.d/functions 34 | fi 35 | 36 | is_func() { 37 | type $1 2>/dev/null | grep -q 'function' 38 | } 39 | 40 | log_success () { 41 | if is_func log_success_msg; then 42 | log_success_msg "$*" 43 | elif is_func success; then 44 | echo -n $*; success "$*"; echo 45 | else 46 | echo "[ ok ] $*" 47 | fi 48 | } 49 | 50 | log_failure () { 51 | if is_func log_failure_msg; then 52 | log_failure_msg "$*" 53 | elif is_func failure; then 54 | echo -n $*; failure "$*"; echo 55 | else 56 | echo "[FAIL] $* ... failed!" 57 | fi 58 | } 59 | 60 | log_warning () { 61 | if is_func log_warning_msg; then 62 | log_warning_msg "$*" 63 | elif is_func warning; then 64 | echo -n $*; warning "$*"; echo 65 | else 66 | echo "[warn] $* ... (warning)." 67 | fi 68 | } 69 | 70 | log_action () { 71 | if is_func log_action_msg; then 72 | log_action_msg "$*" 73 | elif is_func action; then 74 | echo -n $*; passed "$*"; echo 75 | else 76 | echo "[info] $*." 77 | fi 78 | } 79 | 80 | load_specfiles() 81 | { 82 | FABRIC_MODS=$(python << EOF 83 | from rtslib import list_specfiles, parse_specfile 84 | print(" ".join(["%(kernel_module)s:%(configfs_group)s" % parse_specfile(spec) 85 | for spec in list_specfiles()])) 86 | EOF 87 | ) 88 | } 89 | 90 | save_running_config() 91 | { 92 | python << EOF 93 | import rtslib 94 | config = rtslib.Config() 95 | config.load_live() 96 | config.save("${STARTUP_CONFIG}") 97 | EOF 98 | } 99 | 100 | check_install() 101 | { 102 | # Check the system installation 103 | INSTALL=ok 104 | 105 | python -c "from rtslib import Config" > /dev/null 2>&1 106 | if [ $? != 0 ]; then 107 | log_failure "Cannot load rtslib" 108 | INSTALL=nok 109 | fi 110 | 111 | SYSTEM_DIRS="/var/target/pr /var/target/alua /etc/target" 112 | for DIR in ${SYSTEM_DIRS}; do 113 | if [ ! -d ${DIR} ]; then 114 | log_warning "Creating missing directory ${DIR}" 115 | mkdir -p ${DIR} 116 | fi 117 | done 118 | 119 | if [ "${INSTALL}" != ok ]; then 120 | exit 0 121 | else 122 | log_action "${DESC} looks properly installed" 123 | fi 124 | } 125 | 126 | load_configfs() 127 | { 128 | modprobe configfs > /dev/null 2>&1 129 | if [ "$?" != 0 ]; then 130 | log_failure "Failed to load configfs kernel module" 131 | return 1 132 | fi 133 | mount -t configfs configfs ${CFS_BASE} > /dev/null 2>&1 134 | case "$?" in 135 | 0) log_warning "The configfs filesystem was not mounted, consider adding it to fstab";; 136 | 32) log_action "The configfs filesystem is already mounted";; 137 | *) log_failure "Failed to mount configfs"; return 1;; 138 | esac 139 | } 140 | 141 | load_modules() 142 | { 143 | for MODULE in ${CORE_MODS}; do 144 | if [ ! -z "$(cat /proc/modules | grep ^${MODULE}\ )" ]; then 145 | log_warning "Core module ${MODULE} already loaded" 146 | else 147 | modprobe "${MODULE}" > /dev/null 2>&1 148 | if [ "$?" != 0 ]; then 149 | log_failure "Failed to load core module ${MODULE}" 150 | return 1 151 | else 152 | log_action "Loaded core module ${MODULE}" 153 | fi 154 | fi 155 | done 156 | for MOD_SPEC in ${FABRIC_MODS}; do 157 | MODULE="$(echo ${MOD_SPEC} | awk -F : '{print $1}')" 158 | if [ ! -z "$(cat /proc/modules | grep ^${MODULE}\ )" ]; then 159 | log_warning "Fabric module ${MODULE} already loaded" 160 | else 161 | modprobe "${MODULE}" > /dev/null 2>&1 162 | if [ "$?" != 0 ]; then 163 | log_warning "Failed to load fabric module ${MODULE}" 164 | else 165 | log_action "Loaded fabric module ${MODULE}" 166 | fi 167 | fi 168 | done 169 | } 170 | 171 | unload_modules() 172 | { 173 | RETCODE=0 174 | 175 | for MOD_SPEC in ${FABRIC_MODS}; do 176 | MODULE="$(echo ${MOD_SPEC} | awk -F : '{print $1}')" 177 | CFS_GROUP="${CFS_TGT}/$(echo ${MOD_SPEC} | awk -F : '{print $2}')" 178 | if [ ! -z "$(lsmod | grep ^${MODULE}\ )" ]; then 179 | rmdir "${CFS_GROUP}" > /dev/null 2>&1 180 | if [ -d "${CFS_GROUP}" ]; then 181 | log_failure "Failed to remove ${CFS_GROUP}" 182 | RETCODE=1 183 | else 184 | rmmod "${MODULE}" > /dev/null 2>&1 185 | if [ "$?" != 0 ]; then 186 | log_failure "Failed to unload fabric module ${MODULE}" 187 | RETCODE=1 188 | else 189 | log_action "Unloaded ${MODULE} fabric module" 190 | fi 191 | fi 192 | else 193 | log_warning "Fabric module ${MODULE} is not loaded" 194 | fi 195 | done 196 | 197 | MODULES="$(echo ${CORE_MODS} | tac -s ' ')" 198 | for MODULE in ${MODULES}; do 199 | if [ ! -z "$(lsmod | grep ^${MODULE}\ )" ]; then 200 | rmmod "${MODULE}" > /dev/null 2>&1 201 | if [ "$?" != 0 ]; then 202 | log_failure "Failed to unload target core module ${MODULE}" 203 | RETCODE=1 204 | else 205 | log_action "Unloaded ${MODULE} target core module" 206 | fi 207 | else 208 | log_warning "Target core module ${MODULE} is not loaded" 209 | fi 210 | done 211 | 212 | return "${RETCODE}" 213 | } 214 | 215 | load_config() 216 | { 217 | 218 | if [ $(cat /etc/target/scsi_target.lio 2>/dev/null | tr -d " \n\t" | wc -c) = 0 ]; then 219 | log_warning "Startup config ${STARTUP_CONFIG} is empty, skipping" 220 | elif [ -e "${STARTUP_CONFIG}" ]; then 221 | log_action "Loading config from ${STARTUP_CONFIG}, this may take several minutes for FC adapters" 222 | export __STARTUP_CONFIG="${STARTUP_CONFIG}" 223 | python 2> /dev/null << EOF 224 | import os, rtslib 225 | config = rtslib.Config() 226 | config.load(os.environ['__STARTUP_CONFIG'], allow_new_attrs=True) 227 | list(config.apply()) 228 | EOF 229 | if [ "$?" != 0 ]; then 230 | unset __STARTUP_CONFIG 231 | log_failure "Failed to load ${STARTUP_CONFIG}" 232 | return 1 233 | else 234 | unset __STARTUP_CONFIG 235 | log_action "Loaded ${STARTUP_CONFIG}" 236 | fi 237 | else 238 | log_warning "No ${STARTUP_CONFIG} to load" 239 | fi 240 | } 241 | 242 | clear_config() 243 | { 244 | log_action "Clearing configuration, this may take several minutes for FC adapters" 245 | python 2> /dev/null << EOF 246 | from rtslib import Config 247 | config = Config() 248 | list(config.apply()) 249 | EOF 250 | 251 | if [ "$?" != 0 ]; then 252 | log_failure "Failed to clear configuration" 253 | return 1 254 | else 255 | log_action "Cleared configuration" 256 | fi 257 | } 258 | 259 | do_start() 260 | { 261 | # If the target is running and we do not have a config file on the 262 | # system, dump the running system config to the config file. This 263 | # helps migrating away from lio-utils or other legacy/devel systems. 264 | if [ ! -e "${STARTUP_CONFIG}" ] && [ $(ls /sys/kernel/config/target/core/ 2>/dev/null | wc -l) -gt 1 ]; then 265 | log_action "Possible config migration detected, saving the " \ 266 | "running target to ${STARTUP_CONFIG}" 267 | save_running_config 268 | elif [ -d ${CFS_TGT} ]; then 269 | log_failure "Not starting: ${CFS_TGT} already exists" 270 | return 1 271 | fi 272 | 273 | load_specfiles # Fill in FABRIC_MODS and CFS_GROUPS 274 | check_install && load_configfs && load_modules && load_config 275 | if [ "$?" != 0 ]; then 276 | log_failure "Could not start ${DESC}" 277 | return 1 278 | else 279 | log_success "Started ${DESC}" 280 | fi 281 | } 282 | 283 | do_stop() 284 | { 285 | if [ ! -d ${CFS_TGT} ]; then 286 | log_success "${DESC} is already stopped" 287 | else 288 | load_specfiles # Fill in FABRIC_MODS and CFS_GROUPS 289 | clear_config && unload_modules 290 | if [ "$?" != 0 ]; then 291 | log_failure "Could not stop ${DESC}" 292 | return 1 293 | else 294 | log_success "Stopped ${DESC}" 295 | fi 296 | fi 297 | } 298 | 299 | do_status() 300 | { 301 | if [ -d ${CFS_TGT} ]; then 302 | log_action "${DESC} is started" 303 | return 0 304 | else 305 | log_action "${DESC} is stopped" 306 | return 1 307 | fi 308 | } 309 | 310 | case "$1" in 311 | start) 312 | # FIXME This is because stop fails with systemd on debian jessie 313 | do_stop 314 | do_start 315 | ;; 316 | stop) 317 | do_stop 318 | ;; 319 | status) 320 | do_status ;; 321 | restart|force-reload) 322 | do_stop && do_start ;; 323 | *) 324 | echo "Usage: $SCRIPTNAME {start|stop|status|restart|force-reload}" >&2 325 | exit 3 326 | ;; 327 | esac 328 | -------------------------------------------------------------------------------- /scripts/targetcli: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | ''' 3 | Starts the targetcli CLI shell. 4 | 5 | This file is part of LIO(tm). 6 | Copyright (c) 2011-2014 by Datera, Inc 7 | 8 | Licensed under the Apache License, Version 2.0 (the "License"); you may 9 | not use this file except in compliance with the License. You may obtain 10 | a copy of the License at 11 | 12 | http://www.apache.org/licenses/LICENSE-2.0 13 | 14 | Unless required by applicable law or agreed to in writing, software 15 | distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 16 | WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 17 | License for the specific language governing permissions and limitations 18 | under the License. 19 | ''' 20 | 21 | import sys 22 | from os import getuid, listdir 23 | from targetcli import UIRoot 24 | from rtslib import RTSLibError 25 | from configshell import ConfigShell 26 | from rtslib import __version__ as rtslib_version 27 | from targetcli import __version__ as targetcli_version 28 | 29 | class TargetCLI(ConfigShell): 30 | default_prefs = {'color_path': 'magenta', 31 | 'color_command': 'cyan', 32 | 'color_parameter': 'magenta', 33 | 'color_keyword': 'cyan', 34 | 'completions_in_columns': True, 35 | 'logfile': None, 36 | 'loglevel_console': 'info', 37 | 'loglevel_file': 'debug9', 38 | 'color_mode': True, 39 | 'prompt_length': 30, 40 | 'tree_max_depth': 0, 41 | 'tree_status_mode': True, 42 | 'tree_round_nodes': True, 43 | 'tree_show_root': True, 44 | 'auto_enable_tpgt': True, 45 | 'auto_add_mapped_luns': True, 46 | 'auto_cd_after_create': False, 47 | 'legacy_hba_view': False 48 | } 49 | 50 | def main(): 51 | ''' 52 | Start the targetcli shell. 53 | ''' 54 | shell = TargetCLI('~/.targetcli') 55 | 56 | try: 57 | listdir("/sys/kernel/config/target") 58 | except: 59 | shell.con.display("The target service is not running.") 60 | exit() 61 | 62 | if getuid() == 0: 63 | is_root = True 64 | else: 65 | is_root = False 66 | 67 | if not is_root: 68 | shell.con.display("You are not root, disabling privileged commands.\n") 69 | 70 | root_node = UIRoot(shell, as_root=is_root) 71 | 72 | try: 73 | root_node.refresh() 74 | except RTSLibError, error: 75 | shell.con.display(shell.con.render_text(str(error), 'red')) 76 | 77 | if len(sys.argv) > 1: 78 | shell.run_cmdline(" ".join(sys.argv[1:])) 79 | sys.exit(0) 80 | 81 | shell.con.display("targetcli %s (rtslib %s)\n" 82 | "Copyright (c) 2011-2014 by Datera, Inc.\n" 83 | "All rights reserved." 84 | % (targetcli_version, rtslib_version)) 85 | shell.con.display('') 86 | shell.run_interactive() 87 | 88 | if __name__ == "__main__": 89 | main() 90 | -------------------------------------------------------------------------------- /scripts/targetcli-ng: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | ''' 3 | This file is part of LIO(tm). 4 | 5 | Copyright (c) 2012-2014 by Datera, Inc. 6 | More information on www.datera.io. 7 | 8 | Original author: Jerome Martin 9 | 10 | Datera and LIO are trademarks of Datera, Inc., which may be registered in some 11 | jurisdictions. 12 | 13 | Licensed under the Apache License, Version 2.0 (the "License"); you may 14 | not use this file except in compliance with the License. You may obtain 15 | a copy of the License at 16 | 17 | http://www.apache.org/licenses/LICENSE-2.0 18 | 19 | Unless required by applicable law or agreed to in writing, software 20 | distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 21 | WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 22 | License for the specific language governing permissions and limitations 23 | under the License. 24 | ''' 25 | import sys 26 | from pyparsing import ParseException 27 | from rtslib.config import ConfigError 28 | from targetcli.cli_live import CliLive 29 | from targetcli.cli_config import CliConfig 30 | from targetcli.cli_logger import logger as log 31 | 32 | # TODO Add tests for non-interactive mode 33 | # TODO Add batch mode if stdin is not a terminal 34 | 35 | if __name__ == '__main__': 36 | try: 37 | args = sys.argv[1:] 38 | if not args: 39 | config = 'live' 40 | CliLive(interactive=True).cmdloop() 41 | elif args[0] == "configure" and len(args) == 1: 42 | config = 'candidate' 43 | CliConfig(interactive=True).cmdloop() 44 | elif args[0] == "configure": 45 | config = 'candidate' 46 | CliConfig(interactive=False).onecmd(" ".join(args[1:])) 47 | else: 48 | config = 'live' 49 | CliLive(interactive=False).onecmd(" ".join(args)) 50 | 51 | except IOError, e: 52 | log.critical("Failed to read %s configuration: %s" % (config, e)) 53 | log.info("Check your user permissions") 54 | 55 | except ParseException, e: 56 | log.critical("Failed to parse %s configuration: %s" % (config, e)) 57 | 58 | except ParseException, e: 59 | log.critical("Failed to validate %s configuration: %s" % (config, e)) 60 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python 2 | ''' 3 | This file is part of LIO(tm). 4 | Copyright (c) 2011-2014 by Datera, Inc 5 | 6 | Licensed under the Apache License, Version 2.0 (the "License"); you may 7 | not use this file except in compliance with the License. You may obtain 8 | a copy of the License at 9 | 10 | http://www.apache.org/licenses/LICENSE-2.0 11 | 12 | Unless required by applicable law or agreed to in writing, software 13 | distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 14 | WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 15 | License for the specific language governing permissions and limitations 16 | under the License. 17 | ''' 18 | 19 | import re 20 | from distutils.core import setup 21 | import targetcli 22 | 23 | PKG = targetcli 24 | VERSION = str(PKG.__version__) 25 | (AUTHOR, EMAIL) = re.match('^(.*?)\s*<(.*)>$', PKG.__author__).groups() 26 | URL = PKG.__url__ 27 | LICENSE = PKG.__license__ 28 | SCRIPTS = ["scripts/targetcli", "scripts/targetcli-ng"] 29 | DESCRIPTION = PKG.__description__ 30 | 31 | setup(name=PKG.__name__, 32 | description=DESCRIPTION, 33 | version=VERSION, 34 | author=AUTHOR, 35 | author_email=EMAIL, 36 | license=LICENSE, 37 | url=URL, 38 | scripts=SCRIPTS, 39 | packages=[PKG.__name__], 40 | package_data = {'':[]}) 41 | -------------------------------------------------------------------------------- /targetcli/__init__.py: -------------------------------------------------------------------------------- 1 | ''' 2 | This file is part of LIO(tm). 3 | Copyright (c) 2011-2014 by Datera, Inc 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); you may 6 | not use this file except in compliance with the License. You may obtain 7 | a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 13 | WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 14 | License for the specific language governing permissions and limitations 15 | under the License. 16 | ''' 17 | 18 | from ui_root import UIRoot 19 | 20 | __version__ = 'GIT_VERSION' 21 | __author__ = "Jerome Martin " 22 | __url__ = "http://www.risingtidesystems.com" 23 | __description__ = "An administration shell for RTS storage targets." 24 | __license__ = __doc__ 25 | -------------------------------------------------------------------------------- /targetcli/cli.py: -------------------------------------------------------------------------------- 1 | ''' 2 | This file is part of LIO(tm). 3 | 4 | Copyright (c) 2012-2014 by Datera, Inc. 5 | More information on www.datera.io. 6 | 7 | Original author: Jerome Martin 8 | 9 | Datera and LIO are trademarks of Datera, Inc., which may be registered in some 10 | jurisdictions. 11 | 12 | Licensed under the Apache License, Version 2.0 (the "License"); you may 13 | not use this file except in compliance with the License. You may obtain 14 | a copy of the License at 15 | 16 | http://www.apache.org/licenses/LICENSE-2.0 17 | 18 | Unless required by applicable law or agreed to in writing, software 19 | distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 20 | WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 21 | License for the specific language governing permissions and limitations 22 | under the License. 23 | ''' 24 | import pyparsing as pp 25 | import sys, tty, cmd, termios, readline, traceback 26 | 27 | import rtslib.config, rtslib.config_tree 28 | from targetcli.cli_logger import logger as log 29 | from rtslib.config import ConfigError 30 | 31 | # TODO Implement | filters: top N, last N, page, grep 32 | # TODO Redo help summary, using 2 columns: cmd, short description 33 | 34 | class CliError(Exception): 35 | pass 36 | 37 | class Cli(cmd.Cmd): 38 | ''' 39 | Our base Cli class, common to both CliLive and CliConfig 40 | ''' 41 | intro = '' 42 | log_levels = {'debug': 10, 'info': 20, 'warning': 30, 43 | 'error': 40, 'critical': 50} 44 | 45 | def __init__(self, interactive, history_path): 46 | ''' 47 | Initializes a new Cli object. 48 | 49 | interactive is a boolean to run either interactively or in batch mode 50 | history_path is the path to the command-line history file 51 | ''' 52 | cmd.Cmd.__init__(self) 53 | self.debug_level = 'off' 54 | self.last_traceback = None 55 | self.interactive = interactive 56 | self.do_save_history = self.interactive 57 | if self.interactive: 58 | self.load_history() 59 | readline.set_completer_delims(' \t\n`~!@#$%^&*()=+[{]}\\|;\'",<>/?') 60 | 61 | def do_EOF(self, options): 62 | sys.stdout.write("exit\n") 63 | return self.do_exit(options) 64 | 65 | def _complete_options(self, text, line, begidx, endidx, options): 66 | ''' 67 | Helper to autocomplete one or more options out of options, without any 68 | ordering considerations. 69 | ''' 70 | # TODO Add middle-of-line completion 71 | prev_options = line.split()[1:] 72 | if text: 73 | prev_options = prev_options[:-1] 74 | return ["%s " % name for name in options 75 | if name.startswith(text) 76 | if name.strip() not in prev_options] 77 | 78 | def _complete_one_option(self, text, line, begidx, endidx, options): 79 | ''' 80 | Helper to autocomplete a single option out of options. 81 | ''' 82 | # TODO Add middle-of-line completion 83 | prev_options = line.split()[1:] 84 | if text: 85 | prev_options = prev_options[:-1] 86 | return ["%s " % name for name in options 87 | if name.startswith(text) 88 | if not prev_options] 89 | 90 | def _complete_path(self, text, line, begidx, endidx, prefix=None): 91 | ''' 92 | Helper to autocomplete a configuration path. 93 | ''' 94 | # TODO Add middle-of-line completion 95 | pattern = line.partition(' ')[2] 96 | if prefix is None: 97 | prefix = '' 98 | 99 | # Are we completing an attr/obj value/id or a group? 100 | nodes_last_key = self.config.search(("%s %s.*" 101 | % (prefix, pattern)).strip()) 102 | # Or an attr/obj name/class ? 103 | nodes_first_key = [node for node 104 | in self.config.search(("%s %s.* .*" 105 | % (prefix, pattern)).strip()) 106 | if node.data['type'] != 'group'] 107 | completions = [] 108 | completions.extend(node.key[-1] for node in nodes_last_key) 109 | completions.extend(node.key[0] for node in nodes_first_key) 110 | return ["%s " % c for c in completions if c.startswith(text)] 111 | 112 | def _complete_filepath(self, text, line, begidx, endidx): 113 | ''' 114 | Helper to autocomplete file paths. 115 | ''' 116 | # TODO Implement this 117 | return [] 118 | 119 | def save_history(self): 120 | ''' 121 | Saves the command history. 122 | ''' 123 | if not self.do_save_history: 124 | return 125 | try: 126 | readline.write_history_file(self.history_path) 127 | except Exception, e: 128 | raise CliError("Failed to save command history, disabling: %s", e) 129 | self.do_save_history = False 130 | 131 | def load_history(self): 132 | ''' 133 | Loads the command history. 134 | ''' 135 | try: 136 | readline.read_history_file(self.history_path) 137 | except IOError, e: 138 | log.debug("Error while reading history: %s" % e) 139 | 140 | def clear_history(self): 141 | ''' 142 | Clears the command history. 143 | ''' 144 | readline.clear_history() 145 | 146 | def emptyline(self): 147 | ''' 148 | Just go on with a new prompt line if the user enters an empty line. 149 | ''' 150 | pass 151 | 152 | def cmdloop(self): 153 | ''' 154 | The main REPL loop. 155 | ''' 156 | intro = self.intro 157 | while True: 158 | try: 159 | cmd.Cmd.cmdloop(self, intro=intro) 160 | except KeyboardInterrupt: 161 | sys.stdout.write("^C\n") 162 | intro = '' 163 | else: 164 | break 165 | 166 | def onecmd(self, line): 167 | ''' 168 | Executes a command line. 169 | ''' 170 | try: 171 | result = cmd.Cmd.onecmd(self, line) 172 | except pp.ParseException, e: 173 | log.error("Unknown syntax: %s at char %d" % (e.msg, e.loc)) 174 | return None 175 | except ConfigError, e: 176 | self.last_traceback = traceback.format_exc() 177 | log.error(str(e)) 178 | except CliError, e: 179 | self.last_traceback = traceback.format_exc() 180 | log.error(str(e)) 181 | except Exception, e: 182 | self.last_traceback = traceback.format_exc() 183 | log.error("%s: %s\n" % (e.__class__.__name__, e)) 184 | return None 185 | else: 186 | self.save_history() 187 | return result 188 | 189 | def completenames(self, text, *ignored): 190 | return ["%s " % name[3:] for name in self.get_names() 191 | if name.startswith("do_%s" % text) 192 | if not name in ['do_EOF']] 193 | 194 | def getchar(self): 195 | ''' 196 | Returns the first character read from stdin, without waiting for the 197 | user to hit enter. 198 | ''' 199 | fd = sys.stdin.fileno() 200 | tcattr_backup = termios.tcgetattr(fd) 201 | try: 202 | tty.setraw(sys.stdin.fileno()) 203 | char = sys.stdin.read(1) 204 | finally: 205 | termios.tcsetattr(fd, termios.TCSADRAIN, tcattr_backup) 206 | return char 207 | 208 | def yes_no(self, question, default=None): 209 | ''' 210 | Asks a yes/no question to be answered by typing a single 'y' or 'n' 211 | character. If we do not run in interactive mode, returns None. Else 212 | returns True for yes and False for not. 213 | 214 | default can either be True (yes is the default), False (no is the 215 | default) or None (no default). 216 | ''' 217 | keys = {'\x03': '^C', '\x04': '^D'} 218 | if not self.interactive: 219 | result = None 220 | else: 221 | if default is None: 222 | choices = "y/n" 223 | elif default is True: 224 | choices = "Y/n" 225 | dfl_key = 'y' 226 | elif default is False: 227 | choices = "y/N" 228 | dfl_key = 'n' 229 | key = None 230 | replies = ['y', 'n', 'Y', 'N'] 231 | if default is not None: 232 | replies.append('\r') 233 | while key not in replies: 234 | log.debug("Got key %r" % key) 235 | sys.stdout.write("%s [%s] " % (question, choices)) 236 | key = self.getchar() 237 | key = keys.get(key, key) 238 | if key == '\r' and default is not None: 239 | sys.stdout.write("%s\n" % dfl_key) 240 | else: 241 | sys.stdout.write("%s\n" % key) 242 | if key in ['^C', '^D']: 243 | raise CliError("Aborted") 244 | if key == '\r': 245 | result = default 246 | elif key.lower() == 'y': 247 | result = True 248 | else: 249 | result = False 250 | 251 | log.debug("yes_no(%s) -> %r" % (question, result)) 252 | return result 253 | 254 | def parse(self, line, header, grammar): 255 | ''' 256 | Parses line using a pyparsing grammar. 257 | Returns the parse tree as a list. 258 | ''' 259 | if not grammar: 260 | grammar = pp.Empty() 261 | grammar = pp.Literal(header) + grammar 262 | line = "%s %s" % (header, line) 263 | log.debug("Parsing line '%s'" % line) 264 | tokens = grammar.parseString(line, parseAll=True).asList() 265 | log.debug("Got parse tree %s" % tokens) 266 | return tokens 267 | 268 | def do_trace(self, options): 269 | ''' 270 | trace 271 | 272 | Displays the last exception trace for the current mode. 273 | 274 | This is useful only for debugging the application. Your lio support 275 | team might ask you to run this command to help understanding an issue 276 | you're experimenting. 277 | ''' 278 | options = self.parse(options, 'trace', '')[1:] 279 | if self.last_traceback is not None: 280 | log.error(self.last_traceback) 281 | else: 282 | log.error("No previous exception traceback.") 283 | 284 | def do_debug(self, options): 285 | ''' 286 | debug [off|cli|api|all] 287 | 288 | Controls the debug messages level: 289 | 290 | off disables all debug message 291 | cli enables only cli debug messages 292 | api also enables Config API messages 293 | all adds even more details to api debug 294 | 295 | With no option, displays the current debug level. 296 | ''' 297 | syntax = pp.Optional(pp.oneOf(["off", "cli", "api", "all"])) 298 | options = self.parse(options, 'debug', syntax)[1:] 299 | 300 | if not options: 301 | log.info("Current debug level: %s" % self.debug_level) 302 | else: 303 | self.debug_level = options[0] 304 | if self.debug_level == 'off': 305 | log.setLevel(self.log_levels['info']) 306 | rtslib.config.log.setLevel(self.log_levels['info']) 307 | rtslib.config_tree.log.setLevel(self.log_levels['info']) 308 | elif self.debug_level == 'cli': 309 | log.setLevel(self.log_levels['debug']) 310 | rtslib.config.log.setLevel(self.log_levels['info']) 311 | rtslib.config_tree.log.setLevel(self.log_levels['info']) 312 | elif self.debug_level == 'api': 313 | log.setLevel(self.log_levels['debug']) 314 | rtslib.config.log.setLevel(self.log_levels['debug']) 315 | rtslib.config_tree.log.setLevel(self.log_levels['info']) 316 | elif self.debug_level == 'all': 317 | log.setLevel(self.log_levels['debug']) 318 | rtslib.config.log.setLevel(self.log_levels['debug']) 319 | rtslib.config_tree.log.setLevel(self.log_levels['debug']) 320 | 321 | log.info("Debug level is now: %s" % self.debug_level) 322 | 323 | def complete_debug(self, text, line, begidx, endidx): 324 | return self._complete_one_option(text, line, begidx, endidx, 325 | ["off", "cli", "api", "all"]) 326 | -------------------------------------------------------------------------------- /targetcli/cli_config.py: -------------------------------------------------------------------------------- 1 | ''' 2 | This file is part of LIO(tm). 3 | 4 | Copyright (c) 2012-2014 by Datera, Inc. 5 | More information on www.datera.io. 6 | 7 | Original author: Jerome Martin 8 | 9 | Datera and LIO are trademarks of Datera, Inc., which may be registered in some 10 | jurisdictions. 11 | 12 | Licensed under the Apache License, Version 2.0 (the "License"); you may 13 | not use this file except in compliance with the License. You may obtain 14 | a copy of the License at 15 | 16 | http://www.apache.org/licenses/LICENSE-2.0 17 | 18 | Unless required by applicable law or agreed to in writing, software 19 | distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 20 | WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 21 | License for the specific language governing permissions and limitations 22 | under the License. 23 | ''' 24 | import pyparsing as pp 25 | import prettytable as pt 26 | import os, sys, datetime, shutil 27 | 28 | from rtslib.config_filters import * 29 | from targetcli.cli import Cli, CliError 30 | from rtslib.config_live import dump_live 31 | from targetcli.cli_logger import logger as log 32 | from rtslib.config_parser import ConfigParser 33 | from rtslib.config import Config, ConfigError 34 | 35 | # TODO Add path vs pattern documentation 36 | # TODO Implement 'configure locked' mode 37 | # TODO Implement do_copy 38 | # TODO Implement do_comment 39 | # TODO Implement do_rollback 40 | # TODO When live summary is done, use tables for info 41 | # TODO Allow PATH=='top ... ...' to indicate top-level 42 | 43 | class CliConfig(Cli): 44 | ''' 45 | The lio target configuration command-line for edit mode. 46 | ''' 47 | config_path = "/etc/target/scsi_target.lio" 48 | history_path = os.path.expanduser("~/.targetcli/history_configure.txt") 49 | backup_dir = "/var/target" 50 | 51 | @classmethod 52 | def save_running_config(cls): 53 | if os.path.isfile(cls.config_path): 54 | # TODO remove/rotate older backups 55 | ts = datetime.datetime.now().strftime("%Y-%m-%d_%H:%M:%S") 56 | backup_path = "%s/backup-%s.lio" % (cls.backup_dir, ts) 57 | log.info("Performing backup of startup configuration: %s" 58 | % backup_path) 59 | shutil.copyfile(cls.config_path, backup_path) 60 | log.info("Saving new startup configuration") 61 | # We reload the config from live before saving it, in 62 | # case this kernel has new attributes not yet in our 63 | # policy files 64 | config = Config() 65 | config.load_live() 66 | config.save(cls.config_path) 67 | 68 | def __init__(self, interactive=False): 69 | Cli.__init__(self, interactive, self.history_path) 70 | self.set_prompt() 71 | log.info("Syncing policy and configuration...") 72 | self.config = Config() 73 | self.config.load_live() 74 | self.edit_levels = [''] 75 | self.needs_save = False 76 | if interactive: 77 | log.warning("[edit] top-level") 78 | 79 | @property 80 | def needs_commit(self): 81 | if self.needs_save: 82 | return True 83 | keys = ('removed', 'major', 'major_obj', 84 | 'minor', 'minor_obj', 'created') 85 | diff = self.config.diff_live() 86 | for key in keys: 87 | if diff[key]: 88 | return True 89 | return False 90 | 91 | @property 92 | def attrs_missing(self): 93 | for attr in self.config.current.walk(filter_only_missing): 94 | return True 95 | return False 96 | 97 | def add_edit_level(self, path): 98 | self.edit_levels.append(path) 99 | log.warning("[edit] %s" % self.edit_levels[-1]) 100 | self.set_prompt(self.edit_levels[-1]) 101 | 102 | def del_edit_level(self): 103 | if len(self.edit_levels) == 1: 104 | raise CliError("Already at top-level") 105 | 106 | self.edit_levels.pop() 107 | if len(self.edit_levels) == 1: 108 | log.warning("[edit] top-level") 109 | else: 110 | log.warning("[edit] %s" % self.edit_levels[-1]) 111 | self.set_prompt(self.edit_levels[-1]) 112 | 113 | def set_prompt(self, string=''): 114 | ''' 115 | Sets the prompt from string. 116 | ''' 117 | if not string: 118 | prompt = "config# " 119 | else: 120 | max_len = 25 121 | if len(string) <= max_len: 122 | prompt = "%s# " % string 123 | else: 124 | prompt = "..%s# " % string[-max_len+3:] 125 | self.prompt = prompt 126 | 127 | def fmt_data_src(self, src): 128 | 129 | # TODO Get rid of this one in favor of lst_data_src 130 | 131 | def ts2str(ts): 132 | date = datetime.datetime.fromtimestamp(int(ts)) 133 | date = date.strftime('%Y-%m-%d %H:%M:%S') 134 | return date 135 | 136 | try: 137 | date = ts2str(src['timestamp']) 138 | except: 139 | date = "unknown date" 140 | 141 | if src['operation'] == 'set': 142 | fmt = ("(%s) set %s" 143 | % (date, src['data'].strip())) 144 | elif src['operation'] == 'delete': 145 | fmt = ("(%s) delete %s" 146 | % (date, src['pattern'].strip())) 147 | elif src['operation'] == 'load': 148 | mdate = ts2str(src['mtime']) 149 | fmt = ("(%s) load %s (modified %s)" 150 | % (date, src['filepath'], mdate)) 151 | elif src['operation'] == 'update': 152 | mdate = ts2str(src['mtime']) 153 | fmt = ("(%s) merge %s (modified %s)" 154 | % (date, src['filepath'], mdate)) 155 | elif src['operation'] == 'clear': 156 | fmt = ("(%s) cleared config" 157 | % date) 158 | elif src['operation'] == 'resync': 159 | fmt = ("(%s) Synchronized configuration with live system" 160 | % date) 161 | elif src['operation'] == 'init': 162 | fmt = ("(%s) created new configuration" 163 | % date) 164 | else: 165 | fmt = ("(%s) unknown operation" 166 | % date) 167 | return fmt 168 | 169 | def lst_data_src(self, src): 170 | 171 | def ts2str(ts): 172 | date = datetime.datetime.fromtimestamp(int(ts)) 173 | date = date.strftime('%Y-%m-%d %H:%M:%S') 174 | return date 175 | 176 | try: 177 | date = ts2str(src['timestamp']) 178 | except: 179 | date = "unknown date" 180 | 181 | if src['operation'] == 'set': 182 | lst = [date, 'set', src['data'].strip()] 183 | elif src['operation'] == 'delete': 184 | lst = [date, 'delete', src['pattern'].strip()] 185 | elif src['operation'] == 'load': 186 | mdate = ts2str(src['mtime']) 187 | lst = [date, 'load', 188 | "%s\nmodified %s" % (src['filepath'], mdate)] 189 | elif src['operation'] == 'update': 190 | mdate = ts2str(src['mtime']) 191 | lst = [date, 'merge', 192 | "%s\nmodified %s" % (src['filepath'], mdate)] 193 | elif src['operation'] == 'clear': 194 | lst = [date, 'clear', 'n/a'] 195 | elif src['operation'] == 'resync': 196 | lst = [date, 'resync', 'n/a'] 197 | elif src['operation'] == 'init': 198 | lst = [date, 'init', 'n/a'] 199 | else: 200 | lst = [date, 'unknown', 'n/a'] 201 | return lst 202 | 203 | def do_exit(self, options): 204 | ''' 205 | exit [now] 206 | 207 | Exits the current configuration edit level, and goes back to the 208 | previous edit level. If run on the top-level configuration, then exits 209 | config mode. 210 | 211 | If the now option is provided, no confirmation will be asked if there 212 | are uncommitted changes in the current candidate configuration when 213 | exiting the config mode. 214 | ''' 215 | options = self.parse(options, 'exit', pp.Optional('now'))[1:] 216 | 217 | if self.edit_levels[-1]: 218 | self.del_edit_level() 219 | exit = False 220 | elif self.needs_commit: 221 | log.warning("[edit] All non-commited changes will be lost!") 222 | if 'now' in options: 223 | log.warning("[edit] exiting anyway, as requested") 224 | exit = True 225 | else: 226 | exit = self.yes_no("Exit config mode anyway?", False) 227 | else: 228 | exit = True 229 | return exit 230 | 231 | def complete_exit(self, text, line, begidx, endidx): 232 | return self._complete_options(text, line, begidx, endidx, ['now']) 233 | 234 | def do_commit(self, options): 235 | ''' 236 | commit [check|interactive] 237 | 238 | Saves the current configuration to the system startup configuration 239 | file, after applying the changes to the running system. 240 | 241 | If the check option is provided, the current configuration will be 242 | checked but not saved or applied. 243 | 244 | If the interactive option is provided, the user will be able to confirm 245 | or skip every modification to the live system. 246 | ''' 247 | # TODO Add [as DESCRIPTION] option 248 | # TODO Change to commit only current level unless 'all' option 249 | syntax = pp.Optional(pp.oneOf("check interactive")) 250 | options = self.parse(options, 'commit', syntax)[1:] 251 | 252 | if self.attrs_missing: 253 | self.do_missing('') 254 | raise CliError("Cannot validate configuration: " 255 | "required attributes not set") 256 | 257 | if not self.needs_commit: 258 | raise CliError("No changes to commit!") 259 | 260 | log.info("Validating configuration") 261 | for msg in self.config.verify(): 262 | log.info(msg) 263 | if 'check' in options: 264 | return 265 | 266 | do_it = self.yes_no("Apply changes and overwrite system " 267 | "configuration ?", False) 268 | if do_it is not False: 269 | log.info("Applying configuration") 270 | for msg in self.config.apply(): 271 | if 'interactive' in options: 272 | apply = self.yes_no("%s\nPlease confirm" % msg, True) 273 | if apply is False: 274 | log.warning("Aborted commit on user request: " 275 | "please verify system status") 276 | return 277 | else: 278 | log.info(msg) 279 | self.save_running_config() 280 | self.needs_save = False 281 | else: 282 | log.info("Cancelled configuration commit") 283 | 284 | def complete_commit(self, text, line, begidx, endidx): 285 | return self._complete_options(text, line, begidx, endidx, 286 | ['check', 'interactive']) 287 | 288 | def do_rollback(self, options): 289 | ''' 290 | rollback 291 | 292 | Return to the last committed configuration. Only the current 293 | configuration is affected. The commit command can then be used to apply 294 | the rolled-back configuration to the running system. 295 | ''' 296 | # TODO Add more control to directly rollback the n-th version, view 297 | # backup infos before rollback, etc. 298 | backups = sorted(n for n in os.listdir(self.backup_dir) 299 | if n.endswith(".lio")) 300 | if not backups: 301 | raise ConfigError("No backup found") 302 | else: 303 | backup_path = "%s/%s" % (self.backup_dir, backups[-1]) 304 | self.config.load(backup_path) 305 | os.remove(backup_path) 306 | log.warning("Rolled-back to %s" % backup_path) 307 | 308 | def do_edit(self, options): 309 | ''' 310 | edit PATH 311 | 312 | Changes the current configuration edit level to PATH, relative to the 313 | current configuration edit level. If PATH does not exist currently, it 314 | will be created. 315 | ''' 316 | level = self.edit_levels[-1] 317 | nodes = self.config.search("%s %s" % (level, options)) 318 | if not nodes: 319 | nodes_beyond = self.config.search("%s %s .*" % (level, options)) 320 | if nodes_beyond: 321 | raise CliError("Incomplete path: [%s]" % options) 322 | else: 323 | statement = "%s %s" % (self.edit_levels[-1], options) 324 | log.debug("Setting statement '%s'" % statement) 325 | self.config.set(statement) 326 | self.needs_save = True 327 | node = self.config.search(statement)[0] 328 | log.info("Created configuration level: %s" % node.path_str) 329 | self.add_edit_level(node.path_str) 330 | self.do_missing('') 331 | elif len(nodes) > 1: 332 | raise CliError("Ambiguous path: [%s]" % options) 333 | else: 334 | self.add_edit_level(nodes[0].path_str) 335 | self.do_missing('') 336 | 337 | def complete_edit(self, text, line, begidx, endidx): 338 | # TODO Add tips for new path 339 | return self._complete_path(text, line, begidx, endidx, 340 | self.edit_levels[-1]) 341 | 342 | def do_live(self, options): 343 | ''' 344 | live COMMAND 345 | 346 | Executes a single non-interactive command in live mode. 347 | ''' 348 | # TODO Add completion 349 | from targetcli.cli_live import CliLive 350 | CliLive(interactive=False).onecmd(options) 351 | 352 | def do_set(self, options): 353 | ''' 354 | set [PATH] OBJECT IDENTIFIER 355 | set [PATH] ATTRIBUTE VALUE 356 | 357 | Sets either an OBJECT IDENTIFIER (i.e. "disk mydisk") or an ATTRIBUTE 358 | VALUE (i.e. "enable yes"). 359 | ''' 360 | if not options: 361 | raise CliError("Missing required options") 362 | statement = "%s %s" % (self.edit_levels[-1], options) 363 | log.debug("Setting statement '%s'" % statement) 364 | created = self.config.set(statement) 365 | for node in created: 366 | log.info("[%s] has been set" % node.path_str) 367 | if not created: 368 | log.info("Ignored: Current configuration already match statement") 369 | else: 370 | self.needs_save = True 371 | 372 | def complete_set(self, text, line, begidx, endidx): 373 | # TODO Add tips for new path 374 | return self._complete_path(text, line, begidx, endidx, 375 | self.edit_levels[-1]) 376 | 377 | def do_delete(self, options): 378 | ''' 379 | delete [PATH] 380 | 381 | Deletes either all LIO configuration objects at the current edit level, 382 | or only those under PATH relative to the current level. 383 | ''' 384 | path = "%s %s" % (self.edit_levels[-1], options) 385 | if not path.strip(): 386 | raise CliError("Cannot delete top-level configuration") 387 | 388 | nodes = self.config.search(path) 389 | if not nodes: 390 | # TODO Replace all "%s .*" forms with a try_hard arg to search 391 | nodes.extend(self.config.search("%s .*" % path)) 392 | if not nodes: 393 | raise CliError("No configuration objects at path: %s" 394 | % path.strip()) 395 | 396 | # FIXME Use a real tree walk with filter 397 | obj_no = 0 398 | for node in nodes: 399 | if node.data['type'] == 'obj': 400 | obj_no +=1 401 | 402 | if obj_no == 0: 403 | raise CliError("Can't delete attributes, only objects: %s" 404 | % path.strip()) 405 | 406 | do_it = self.yes_no("Delete %d objects(s) from current configuration?" 407 | % len(nodes), False) 408 | if do_it is not False: 409 | deleted = self.config.delete(path) 410 | if not deleted: 411 | deleted = self.config.delete("%s .*" % path) 412 | self.needs_save = True 413 | log.info("Deleted %d configuration object(s)" % obj_no) 414 | else: 415 | log.info("Cancelled: configuration not modified") 416 | 417 | def complete_delete(self, text, line, begidx, endidx): 418 | # TODO Filter for objects only, skip attributes 419 | return self._complete_path(text, line, begidx, endidx, 420 | self.edit_levels[-1]) 421 | 422 | def do_undo(self, options): 423 | ''' 424 | undo 425 | 426 | Undo the last configuration change done during this config mode 427 | session. The lio cli has unlimited undo levels capabilities within a 428 | session. 429 | 430 | To restore a previously commited configuration, see the rollback 431 | command. 432 | ''' 433 | options = self.parse(options, 'undo', '')[1:] 434 | data_src = self.config.current.data['source'] 435 | self.config.undo() 436 | self.needs_save = True 437 | 438 | # TODO Implement info option to view all previous ops 439 | # TODO Implement last N option for multiple undo 440 | 441 | log.info("[undo] %s" % self.fmt_data_src(data_src)) 442 | 443 | def do_info(self, options): 444 | ''' 445 | info [PATH] 446 | 447 | Displays edit history information about the current configuration level 448 | or all configuration items matching PATH. 449 | ''' 450 | # TODO Add node type information 451 | path = "%s %s" % (self.edit_levels[-1], options) 452 | if not path.strip(): 453 | # This is just a test for tables 454 | table = pt.PrettyTable() 455 | table.hrules = pt.ALL 456 | table.field_names = ["change", "date", "type", "data"] 457 | table.align['data'] = 'l' 458 | changes = [] 459 | nb_ver = len(self.config._configs) 460 | for idx, cfg in enumerate(reversed(self.config._configs)): 461 | lst_src = self.lst_data_src(cfg.data['source']) 462 | table.add_row(["%03d" % (idx + 1)] + lst_src) 463 | # FIXME Use term width to compute these 464 | table.max_width["date"] = 10 465 | table.max_width["data"] = 43 466 | sys.stdout.write("%s\n" % table.get_string()) 467 | else: 468 | nodes = self.config.search(path) 469 | if not nodes: 470 | # TODO Replace all "%s .*" forms with a try_hard arg to search 471 | nodes.extend(self.config.search("%s .*" % path)) 472 | if not nodes: 473 | raise CliError("Path does not exist: %s" % path.strip()) 474 | infos = [] 475 | for node in nodes: 476 | if node.data.get('required'): 477 | req = "(required attribute) " 478 | else: 479 | req = "" 480 | path = node.path_str 481 | infos.append("%s[%s]\nLast change: %s" 482 | % (req, path, 483 | self.fmt_data_src(node.data['source']))) 484 | log.info("\n\n".join(infos)) 485 | 486 | def complete_info(self, text, line, begidx, endidx): 487 | return self._complete_path(text, line, begidx, endidx, 488 | self.edit_levels[-1]) 489 | 490 | def do_clear(self, options): 491 | ''' 492 | clear 493 | 494 | Clears the current configuration. This removes all current objects and 495 | attributes from the configuration. 496 | ''' 497 | options = self.parse(options, 'clear', '')[1:] 498 | 499 | self.config.clear() 500 | log.info("Configuration cleared") 501 | 502 | def do_load(self, options): 503 | ''' 504 | load live|FILE_PATH 505 | 506 | Replaces the current configuration with the contents of FILE_PATH. 507 | If any error happens while doing so, the current configuration will 508 | be fully rolled back. 509 | 510 | If live is used instead of FILE_PATH, the configuration from the live 511 | system will be used instead. 512 | ''' 513 | # TODO Add completion for filepath 514 | # TODO Add a filepath type to policy and also a parser we can use here 515 | tok_string = (pp.QuotedString('"') 516 | | pp.QuotedString("'") 517 | | pp.Word(pp.printables, excludeChars="{}#'\";")) 518 | options = self.parse(options, 'load', tok_string)[1:] 519 | src = options[0] 520 | if src == 'live': 521 | if self.yes_no("Replace the current configuration with the " 522 | "running configuration?", False) is not False: 523 | self.config.load_live() 524 | else: 525 | log.info("Cancelled: configuration not modified") 526 | else: 527 | if self.yes_no("Replace the current configuration with %s?" 528 | % src, False) is not False: 529 | self.config.load(src) 530 | else: 531 | log.info("Cancelled: configuration not modified") 532 | 533 | def do_reload(self, options): 534 | ''' 535 | reload 536 | 537 | Reloads the saved system configuration from disk and commits it to the 538 | running system. 539 | ''' 540 | if not os.path.isfile(self.config_path): 541 | log.info("There is no on-disk system configuration: %s does " 542 | "not exist" % self.config_path) 543 | return 544 | if self.yes_no("Replace the current configuration with the saved " 545 | "system configuration?", False) is not False: 546 | log.info("Loading %s..." % self.config_path) 547 | self.config.load(self.config_path, allow_new_attrs=True) 548 | log.info("Commiting...") 549 | for msg in self.config.apply(): 550 | if 'interactive' in options: 551 | apply = self.yes_no("%s\nPlease confirm" % msg, True) 552 | if apply is False: 553 | log.warning("Aborted commit on user request: " 554 | "please verify system status") 555 | return 556 | else: 557 | log.info(msg) 558 | else: 559 | log.info("Cancelled: configuration not modified") 560 | 561 | def complete_load(self, text, line, begidx, endidx): 562 | # TODO Add filename support 563 | return self._complete_options(text, line, begidx, endidx, ['live']) 564 | 565 | def do_merge(self, options): 566 | ''' 567 | merge live|FILE_PATH 568 | 569 | Merges the contents of FILE_PATH with the current configuration. 570 | In case of conflict, values from FILE_PATH will be used. 571 | If any error happens while doing so, the current configuration will 572 | be fully rolled back. 573 | 574 | If live is used instead of FILE_PATH, the configuration from the live 575 | system will be used instead. 576 | ''' 577 | # TODO Add completion for filepath 578 | # TODO Add a filepath type to policy and also a parser we can use here 579 | tok_string = (pp.QuotedString('"') 580 | | pp.QuotedString("'") 581 | | pp.Word(pp.printables, excludeChars="{}#'\";")) 582 | options = self.parse(options, 'merge', tok_string)[1:] 583 | src = options[0] 584 | if src == 'live': 585 | if self.yes_no("Merge the running configuration with " 586 | "the current configuration?", False) is not False: 587 | self.config.set(dump_live()) 588 | else: 589 | log.info("Cancelled: configuration not modified") 590 | else: 591 | if self.yes_no("Merge %s with the current configuration?" 592 | % src, False) is not False: 593 | self.config.update(src) 594 | else: 595 | log.info("Cancelled: configuration not modified") 596 | 597 | def complete_merge(self, text, line, begidx, endidx): 598 | # TODO Add filename support 599 | return self._complete_options(text, line, begidx, endidx, ['live']) 600 | 601 | def do_dump(self, options): 602 | ''' 603 | dump FILE_PATH [PATH|all] 604 | 605 | Dumps a copy of either the current configuration level or the 606 | configuration at PATH to FILE_PATH. If PATH is 'all', then the 607 | top-level configuration will be dumped. 608 | ''' 609 | options = options.split() 610 | if len(options) < 1: 611 | raise CliError("Syntax error: expected at least one option") 612 | filepath = options.pop(0) 613 | if not filepath.startswith('/'): 614 | raise CliError("Expected an absolute file path") 615 | path = " ".join(options) 616 | if path.strip() == 'all': 617 | path = '' 618 | else: 619 | path = ("%s %s" % (self.edit_levels[-1], path)).strip() 620 | 621 | self.config.save(filepath, path) 622 | if not path: 623 | path_desc = 'all' 624 | else: 625 | path_desc = path 626 | # FIXME Accept "half-node" path 627 | log.info("Dumped [%s] to %s" % (path_desc, filepath)) 628 | 629 | def complete_dump(self, text, line, begidx, endidx): 630 | options = line.split()[1:] 631 | if len(options) < 1: 632 | return self._complete_filepath(text, options[0], 633 | begidx, endidx) 634 | else: 635 | # FIXME This is broken 636 | return self._complete_path(text, " ".join(options[1:]), 637 | begidx, endidx, self.edit_levels[-1]) 638 | 639 | def do_show(self, options): 640 | ''' 641 | show [all] [PATH] 642 | 643 | Shows the current candidate configuration for PATH, relative to the 644 | current edit level. 645 | 646 | Note that attributes with default values will be 647 | filrered out by default, unless the all option is used. 648 | ''' 649 | if options and options.split()[0] == 'all': 650 | options = " ".join(options.split()[1:]) 651 | node_filter = lambda x:x 652 | else: 653 | node_filter = filter_no_default 654 | 655 | path = ("%s %s" % (self.edit_levels[-1], options)).strip() 656 | config = self.config.dump(path, node_filter) 657 | if config is None: 658 | config = self.config.dump("%s .*" % path, node_filter) 659 | if config is not None: 660 | sys.stdout.write("%s\n" % config) 661 | else: 662 | log.error("No such path in current configuration: %s" % path) 663 | 664 | def complete_show(self, text, line, begidx, endidx): 665 | # TODO add all option 666 | return self._complete_path(text, line, begidx, endidx, 667 | self.edit_levels[-1]) 668 | 669 | def do_missing(self, options): 670 | ''' 671 | missing [PATH] 672 | 673 | Shows all missing required attribute values in the current candidate 674 | configuration for PATH, relative to the current edit level. 675 | ''' 676 | node_filter = filter_only_missing 677 | path = ("%s %s" % (self.edit_levels[-1], options)).strip() 678 | if not path: 679 | path = '.*' 680 | trees = self.config.search(path) 681 | if not trees: 682 | trees = self.config.search("%s .*" % path) 683 | if not trees: 684 | raise CliError("No such path: %s" % path) 685 | 686 | missing = [] 687 | for tree in trees: 688 | for attr in tree.walk(node_filter): 689 | missing.append(attr) 690 | 691 | if not options: 692 | path = "current configuration" 693 | 694 | if not missing: 695 | log.warning("No missing attributes values under %s" % path) 696 | else: 697 | log.warning("Missing attributes values under %s:" % path) 698 | for attr in missing: 699 | log.info(" %s" % attr.path_str) 700 | sys.stdout.write("\n") 701 | 702 | def complete_missing(self, text, line, begidx, endidx): 703 | return self._complete_path(text, line, begidx, endidx, 704 | self.edit_levels[-1]) 705 | 706 | def do_diff(self, options): 707 | ''' 708 | diff 709 | 710 | Shows all differences between the current configuration and the live 711 | running configuration. 712 | ''' 713 | options = self.parse(options, 'diff', '')[1:] 714 | diff = self.config.diff_live() 715 | has_diffs = False 716 | if diff['removed']: 717 | has_diffs = True 718 | log.warning("Objects removed in the current configuration:") 719 | for node in diff['removed']: 720 | log.info(" %s" % node.path_str) 721 | if diff['created']: 722 | has_diffs = True 723 | log.warning("New objects in the current configuration:") 724 | for node in diff['created']: 725 | log.info(" %s" % node.path_str) 726 | if diff['major']: 727 | has_diffs = True 728 | log.warning("Major attribute changes in the current configuration:") 729 | for node in diff['major']: 730 | log.info(" %s" % node.path_str) 731 | if diff['minor']: 732 | has_diffs = True 733 | log.warning("Minor attribute changes in the current configuration:") 734 | for node in diff['minor']: 735 | log.info(" %s" % node.path_str) 736 | if not has_diffs: 737 | log.warning("Current configuration is in sync with live system") 738 | else: 739 | sys.stdout.write("\n") 740 | -------------------------------------------------------------------------------- /targetcli/cli_live.py: -------------------------------------------------------------------------------- 1 | ''' 2 | This file is part of LIO(tm). 3 | 4 | Copyright (c) 2012-2014 by Datera, Inc. 5 | More information on www.datera.io. 6 | 7 | Original author: Jerome Martin 8 | 9 | Datera and LIO are trademarks of Datera, Inc., which may be registered in some 10 | jurisdictions. 11 | 12 | Licensed under the Apache License, Version 2.0 (the "License"); you may 13 | not use this file except in compliance with the License. You may obtain 14 | a copy of the License at 15 | 16 | http://www.apache.org/licenses/LICENSE-2.0 17 | 18 | Unless required by applicable law or agreed to in writing, software 19 | distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 20 | WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 21 | License for the specific language governing permissions and limitations 22 | under the License. 23 | ''' 24 | import os, sys 25 | import pyparsing as pp 26 | 27 | from rtslib.config_filters import * 28 | from targetcli.cli import Cli, CliError 29 | from targetcli.cli_config import CliConfig 30 | from targetcli.cli_logger import logger as log 31 | from rtslib.config import Config, ConfigError 32 | 33 | # TODO Implement do_summary using tables + color 34 | # TODO Implement sum for PR 35 | # TODO Implement sum for initiator sessions 36 | # TODO Implement sum for alua metadata 37 | # TODO Implement sum + mgmt for fabric modules 38 | # TODO Implement sum for network BW + portals 39 | # TODO Implement sum for disk IO 40 | 41 | class CliLive(Cli): 42 | ''' 43 | The lio target configuration command-line for live mode. 44 | ''' 45 | history_path = os.path.expanduser("~/.targetcli/history_live.txt") 46 | intro = ("\nWelcome to the lio target interactive shell.\n" 47 | "Copyright (c) 2012-2014 by Datera, Inc.\n" 48 | "Enter '?' to list available commands.\n") 49 | 50 | def __init__(self, interactive=False): 51 | Cli.__init__(self, interactive, self.history_path) 52 | self.prompt = "live> " 53 | self.do_resync() 54 | 55 | def do_exit(self, options): 56 | ''' 57 | exit 58 | 59 | Exits the lio target configuration shell. 60 | ''' 61 | options = self.parse(options, 'exit', '') 62 | return True 63 | 64 | def do_resync(self, options=''): 65 | ''' 66 | resync 67 | 68 | Re-synchronizes the cli with the live running configuration. This 69 | could be useful in rare cases where manual changes have been made to 70 | the underlying configfs structure for debugging purposes. 71 | ''' 72 | options = self.parse(options, 'resync', '') 73 | log.info("Syncing policy and configuration...") 74 | # FIXME Investigate bug in ConfigTree code: error if loading live twice 75 | # without recreating the Config object. 76 | self.config = Config() 77 | self.config.load_live() 78 | 79 | def do_configure(self, options): 80 | ''' 81 | configure 82 | 83 | Switch to config mode. In this mode, you can safely edit a candidate 84 | configuration for the system, and commit it only when it is ready. 85 | ''' 86 | options = self.parse(options, 'configure', '') 87 | if not self.interactive: 88 | raise CliError("Cannot switch to config mode when running " 89 | "non-interactively.") 90 | else: 91 | self.save_history() 92 | self.clear_history() 93 | # FIXME Preserve CliConfig session state, notably undo history 94 | CliConfig(interactive=True).cmdloop() 95 | self.clear_history() 96 | self.load_history() 97 | self.do_resync() 98 | log.warning("[live] Back to live mode") 99 | 100 | def do_show(self, options): 101 | ''' 102 | show [all] [PATH] 103 | 104 | Shows the running live configuration for PATH. 105 | 106 | Note that attributes with default values will be 107 | filrered out by default, unless the all option is used. 108 | ''' 109 | if options and options.split()[0] == 'all': 110 | options = " ".join(options.split()[1:]) 111 | node_filter = lambda x:x 112 | else: 113 | node_filter = filter_no_default 114 | 115 | config = self.config.dump(options, node_filter) 116 | if config is None: 117 | config = self.config.dump("%s .*" % options, node_filter) 118 | if config is not None: 119 | sys.stdout.write("%s\n" % config) 120 | else: 121 | log.error("No such path in current configuration: %s" % options) 122 | 123 | def complete_show(self, text, line, begidx, endidx): 124 | # TODO add all option 125 | return self._complete_path(text, line, begidx, endidx) 126 | 127 | def do_initialize_system(self, options): 128 | ''' 129 | initialize_system 130 | 131 | Loads and commits the system startup configuration if it exists. 132 | ''' 133 | self.config.load(CliConfig.config_path) 134 | do_it = self.yes_no("Load and commit the system startup configuration?" 135 | , False) 136 | if do_it is not False: 137 | log.info("Initializing LIO target...") 138 | for msg in self.config.apply(): 139 | log.info(msg) 140 | self.config.load_live() 141 | 142 | -------------------------------------------------------------------------------- /targetcli/cli_logger.py: -------------------------------------------------------------------------------- 1 | ''' 2 | This file is part of LIO(tm). 3 | 4 | Copyright (c) 2012-2014 by Datera, Inc. 5 | More information on www.datera.io. 6 | 7 | Original author: Jerome Martin 8 | 9 | Datera and LIO are trademarks of Datera, Inc., which may be registered in some 10 | jurisdictions. 11 | 12 | Licensed under the Apache License, Version 2.0 (the "License"); you may 13 | not use this file except in compliance with the License. You may obtain 14 | a copy of the License at 15 | 16 | http://www.apache.org/licenses/LICENSE-2.0 17 | 18 | Unless required by applicable law or agreed to in writing, software 19 | distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 20 | WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 21 | License for the specific language governing permissions and limitations 22 | under the License. 23 | ''' 24 | import sys, logging 25 | 26 | class LogFormatter(logging.Formatter): 27 | 28 | default_format = "LOG%(levelno)s: %(msg)s" 29 | formats = {10: "DEBUG:%(module)s:%(lineno)s: %(msg)s", 30 | 20: "%(msg)s", 31 | 30: "\n### %(msg)s\n", 32 | 40: "*** %(msg)s", 33 | 50: "CRITICAL: %(msg)s"} 34 | 35 | def __init__(self): 36 | logging.Formatter.__init__(self) 37 | 38 | def format(self, record): 39 | self._fmt = self.formats.get(record.levelno, self.default_format) 40 | return logging.Formatter.format(self, record) 41 | 42 | logger = logging.getLogger("LioCli") 43 | logger.setLevel(logging.INFO) 44 | 45 | log_fmt = LogFormatter() 46 | log_handler = logging.StreamHandler(sys.stdout) 47 | log_handler.setFormatter(log_fmt) 48 | logging.root.addHandler(log_handler) 49 | -------------------------------------------------------------------------------- /targetcli/ui_backstore.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Implements the targetcli backstores related UI. 3 | 4 | This file is part of LIO(tm). 5 | Copyright (c) 2011-2014 by Datera, Inc 6 | 7 | Licensed under the Apache License, Version 2.0 (the "License"); you may 8 | not use this file except in compliance with the License. You may obtain 9 | a copy of the License at 10 | 11 | http://www.apache.org/licenses/LICENSE-2.0 12 | 13 | Unless required by applicable law or agreed to in writing, software 14 | distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 15 | WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 16 | License for the specific language governing permissions and limitations 17 | under the License. 18 | ''' 19 | import os 20 | from ui_node import UINode, UIRTSLibNode 21 | from rtslib import RTSRoot 22 | from rtslib import FileIOBackstore, IBlockBackstore 23 | from rtslib import PSCSIBackstore, RDMCPBackstore 24 | from rtslib import FileIOStorageObject, IBlockStorageObject 25 | from rtslib import PSCSIStorageObject, RDMCPStorageObject 26 | from rtslib.utils import get_block_type, is_disk_partition 27 | from rtslib.utils import convert_human_to_bytes, convert_bytes_to_human 28 | from configshell import ExecutionError 29 | 30 | def dedup_so_name(storage_object): 31 | ''' 32 | Useful for migration from ui_backstore_legacy to new style with 33 | 1:1 hba:so mapping. If name is a duplicate in a backstore, returns 34 | name_X where X is the HBA index. 35 | ''' 36 | names = [so.name for so in RTSRoot().storage_objects 37 | if so.backstore.plugin == storage_object.backstore.plugin] 38 | if names.count(storage_object.name) > 1: 39 | return "%s_%d" % (storage_object.name, 40 | storage_object.backstore.index) 41 | else: 42 | return storage_object.name 43 | 44 | 45 | class UIBackstores(UINode): 46 | ''' 47 | The backstores container UI. 48 | ''' 49 | def __init__(self, parent): 50 | UINode.__init__(self, 'backstores', parent) 51 | self.cfs_cwd = "%s/core" % self.cfs_cwd 52 | self.refresh() 53 | 54 | def refresh(self): 55 | self._children = set([]) 56 | UIPSCSIBackstore(self) 57 | UIRDMCPBackstore(self) 58 | UIFileIOBackstore(self) 59 | UIIBlockBackstore(self) 60 | 61 | 62 | class UIBackstore(UINode): 63 | ''' 64 | A backstore UI. 65 | Abstract Base Class, do not instantiate. 66 | ''' 67 | def __init__(self, plugin, parent): 68 | UINode.__init__(self, plugin, parent) 69 | self.cfs_cwd = "%s/core" % self.cfs_cwd 70 | self.refresh() 71 | 72 | def refresh(self): 73 | self._children = set([]) 74 | for so in RTSRoot().storage_objects: 75 | if so.backstore.plugin == self.name: 76 | ui_so = UIStorageObject(so, self) 77 | ui_so.name = dedup_so_name(so) 78 | 79 | def summary(self): 80 | no_storage_objects = len(self._children) 81 | if no_storage_objects > 1: 82 | msg = "%d Storage Objects" % no_storage_objects 83 | else: 84 | msg = "%d Storage Object" % no_storage_objects 85 | return (msg, None) 86 | 87 | def prm_buffered(self, buffered): 88 | buffered = \ 89 | self.ui_eval_param(buffered, 'bool', True) 90 | if buffered: 91 | self.shell.log.info("Using buffered mode.") 92 | else: 93 | self.shell.log.info("Not using buffered mode.") 94 | return buffered 95 | 96 | def ui_command_delete(self, name): 97 | ''' 98 | Recursively deletes the storage object having the specified I{name}. If 99 | there are LUNs using this storage object, they will be deleted too. 100 | 101 | EXAMPLE 102 | ======= 103 | B{delete mystorage} 104 | ------------------- 105 | Deletes the storage object named mystorage, and all associated LUNs. 106 | ''' 107 | self.assert_root() 108 | try: 109 | child = self.get_child(name) 110 | except ValueError: 111 | self.shell.log.error("No storage object named %s." % name) 112 | else: 113 | hba = child.rtsnode.backstore 114 | child.rtsnode.delete() 115 | if not list(hba.storage_objects): 116 | hba.delete() 117 | self.remove_child(child) 118 | self.shell.log.info("Deleted storage object %s." % name) 119 | self.parent.parent.refresh() 120 | 121 | def ui_complete_delete(self, parameters, text, current_param): 122 | ''' 123 | Parameter auto-completion method for user command delete. 124 | @param parameters: Parameters on the command line. 125 | @type parameters: dict 126 | @param text: Current text of parameter being typed by the user. 127 | @type text: str 128 | @param current_param: Name of parameter to complete. 129 | @type current_param: str 130 | @return: Possible completions 131 | @rtype: list of str 132 | ''' 133 | if current_param == 'name': 134 | names = [child.name for child in self.children] 135 | completions = [name for name in names 136 | if name.startswith(text)] 137 | else: 138 | completions = [] 139 | 140 | if len(completions) == 1: 141 | return [completions[0] + ' '] 142 | else: 143 | return completions 144 | 145 | def next_hba_index(self): 146 | self.shell.log.debug("%r" % [(backstore.plugin, backstore.index) 147 | for backstore in RTSRoot().backstores]) 148 | indexes = [backstore.index for backstore in RTSRoot().backstores 149 | if backstore.plugin == self.name] 150 | self.shell.log.debug("Existing %s backstore indexes: %r" 151 | % (self.name, indexes)) 152 | for index in range(1048576): 153 | if index not in indexes: 154 | backstore_index = index 155 | break 156 | 157 | if backstore_index is None: 158 | raise ExecutionError("Cannot find an available backstore index.") 159 | else: 160 | self.shell.log.debug("First available %s backstore index is %d." 161 | % (self.name, backstore_index)) 162 | return backstore_index 163 | 164 | def assert_available_so_name(self, name): 165 | names = [child.name for child in self.children] 166 | if name in names: 167 | raise ExecutionError("Storage object %s/%s already exist." 168 | % (self.name, name)) 169 | 170 | 171 | class UIPSCSIBackstore(UIBackstore): 172 | ''' 173 | PSCSI backstore UI. 174 | ''' 175 | def __init__(self, parent): 176 | UIBackstore.__init__(self, 'pscsi', parent) 177 | 178 | def ui_command_create(self, name, dev): 179 | ''' 180 | Creates a PSCSI storage object, with supplied name and SCSI device. The 181 | SCSI device I{dev} can either be a path name to the device, in which 182 | case it is recommended to use the /dev/disk/by-id hierarchy to have 183 | consistent naming should your physical SCSI system be modified, or an 184 | SCSI device ID in the H:C:T:L format, which is not recommended as SCSI 185 | IDs may vary in time. 186 | ''' 187 | self.assert_root() 188 | self.assert_available_so_name(name) 189 | backstore = PSCSIBackstore(self.next_hba_index(), mode='create') 190 | 191 | if get_block_type(dev) is not None or is_disk_partition(dev): 192 | self.shell.log.info("Note: block backstore recommended for " 193 | "SCSI block devices") 194 | 195 | try: 196 | so = PSCSIStorageObject(backstore, name, dev) 197 | except Exception, exception: 198 | backstore.delete() 199 | raise exception 200 | ui_so = UIStorageObject(so, self) 201 | self.shell.log.info("Created pscsi storage object %s using %s" 202 | % (name, dev)) 203 | return self.new_node(ui_so) 204 | 205 | 206 | class UIRDMCPBackstore(UIBackstore): 207 | ''' 208 | RDMCP backstore UI. 209 | ''' 210 | def __init__(self, parent): 211 | UIBackstore.__init__(self, 'rd_mcp', parent) 212 | 213 | def ui_command_create(self, name, size, nullio=None): 214 | ''' 215 | Creates an RDMCP storage object. I{size} is the size of the ramdisk, 216 | and the optional I{nullio} parameter is a boolean specifying 217 | whether or not we should use a stub nullio instead of a real ramdisk. 218 | 219 | SIZE SYNTAX 220 | =========== 221 | - If size is an int, it represents a number of bytes. 222 | - If size is a string, the following units can be used: 223 | - B{B} or no unit present for bytes 224 | - B{k}, B{K}, B{kB}, B{KB} for kB (kilobytes) 225 | - B{m}, B{M}, B{mB}, B{MB} for MB (megabytes) 226 | - B{g}, B{G}, B{gB}, B{GB} for GB (gigabytes) 227 | - B{t}, B{T}, B{tB}, B{TB} for TB (terabytes) 228 | ''' 229 | self.assert_root() 230 | self.assert_available_so_name(name) 231 | backstore = RDMCPBackstore(self.next_hba_index(), mode='create') 232 | nullio = self.ui_eval_param(nullio, 'bool', False) 233 | try: 234 | so = RDMCPStorageObject(backstore, name, size, nullio=nullio) 235 | 236 | except Exception, exception: 237 | backstore.delete() 238 | raise exception 239 | ui_so = UIStorageObject(so, self) 240 | self.shell.log.info("Created rd_mcp ramdisk %s with size %s." 241 | % (name, size)) 242 | if nullio and not so.nullio: 243 | self.shell.log.warning("nullio ramdisk is not supported by this " 244 | "kernel version, created with nullio=false") 245 | return self.new_node(ui_so) 246 | 247 | 248 | class UIFileIOBackstore(UIBackstore): 249 | ''' 250 | FileIO backstore UI. 251 | ''' 252 | def __init__(self, parent): 253 | UIBackstore.__init__(self, 'fileio', parent) 254 | 255 | def _create_file(self, filename, size, sparse=True): 256 | f = open(filename, "w+") 257 | try: 258 | if sparse: 259 | os.ftruncate(f.fileno(), size) 260 | else: 261 | self.shell.log.info("Writing %s bytes" % size) 262 | while size > 0: 263 | write_size = min(size, 1024) 264 | f.write("\0" * write_size) 265 | size -= write_size 266 | except IOError: 267 | f.close() 268 | os.remove(filename) 269 | raise ExecutionError("Could not expand file to size") 270 | f.close() 271 | 272 | def ui_command_create(self, name, file_or_dev, size=None, 273 | buffered=None, sparse=None): 274 | 275 | ''' 276 | Creates a FileIO storage object. If I{file_or_dev} is a path to a 277 | regular file to be used as backend, then the I{size} parameter is 278 | mandatory. Else, if I{file_or_dev} is a path to a block device, the 279 | size parameter B{must} be ommited. If present, I{size} is the size of 280 | the file to be used, I{file} the path to the file or I{dev} the path to 281 | a block device. The I{buffered} parameter is a boolean stating 282 | whether or not to enable buffered mode. It is enabled by default 283 | (asynchronous mode). The I{sparse} parameter is only applicable when 284 | creating a new backing file. It is a boolean stating if the 285 | created file should be created as a sparse file (the default), or 286 | fully initialized. 287 | 288 | SIZE SYNTAX 289 | =========== 290 | - If size is an int, it represents a number of bytes. 291 | - If size is a string, the following units can be used: 292 | - B{B} or no unit present for bytes 293 | - B{k}, B{K}, B{kB}, B{KB} for kB (kilobytes) 294 | - B{m}, B{M}, B{mB}, B{MB} for MB (megabytes) 295 | - B{g}, B{G}, B{gB}, B{GB} for GB (gigabytes) 296 | - B{t}, B{T}, B{tB}, B{TB} for TB (terabytes) 297 | ''' 298 | self.assert_root() 299 | self.assert_available_so_name(name) 300 | self.shell.log.debug("Using params size=%s buffered=%s" 301 | " sparse=%s" 302 | % (size, buffered, sparse)) 303 | 304 | sparse = self.ui_eval_param(sparse, 'bool', True) 305 | 306 | backstore = FileIOBackstore(self.next_hba_index(), mode='create') 307 | 308 | is_dev = get_block_type(file_or_dev) is not None \ 309 | or is_disk_partition(file_or_dev) 310 | 311 | if size is None and is_dev: 312 | backstore = FileIOBackstore(self.next_hba_index(), mode='create') 313 | try: 314 | so = FileIOStorageObject( 315 | backstore, name, file_or_dev, 316 | buffered_mode=self.prm_buffered(buffered)) 317 | except Exception, exception: 318 | backstore.delete() 319 | raise exception 320 | self.shell.log.info("Created fileio %s with size %s." 321 | % (name, size)) 322 | self.shell.log.info("Note: block backstore preferred for " 323 | " best results.") 324 | ui_so = UIStorageObject(so, self) 325 | return self.new_node(ui_so) 326 | elif size is not None and not is_dev: 327 | backstore = FileIOBackstore(self.next_hba_index(), mode='create') 328 | try: 329 | so = FileIOStorageObject( 330 | backstore, name, file_or_dev, 331 | size, 332 | buffered_mode=self.prm_buffered(buffered)) 333 | except Exception, exception: 334 | backstore.delete() 335 | raise exception 336 | self.shell.log.info("Created fileio %s." % name) 337 | ui_so = UIStorageObject(so, self) 338 | return self.new_node(ui_so) 339 | else: 340 | # use given file size only if backing file does not exist 341 | if os.path.isfile(file_or_dev): 342 | new_size = str(os.path.getsize(file_or_dev)) 343 | if size: 344 | self.shell.log.info("%s exists, using its size (%s bytes)" 345 | " instead" 346 | % (file_or_dev, new_size)) 347 | size = new_size 348 | elif os.path.exists(file_or_dev): 349 | raise ExecutionError("Path %s exists but is not a file" % file_or_dev) 350 | else: 351 | # create file and extend to given file size 352 | if not size: 353 | raise ExecutionError("Attempting to create file for new" + 354 | " fileio backstore, need a size") 355 | self._create_file(file_or_dev, convert_human_to_bytes(size), 356 | sparse) 357 | 358 | 359 | class UIIBlockBackstore(UIBackstore): 360 | ''' 361 | IBlock backstore UI. 362 | ''' 363 | def __init__(self, parent): 364 | UIBackstore.__init__(self, 'iblock', parent) 365 | 366 | def ui_command_create(self, name, dev): 367 | ''' 368 | Creates an IBlock Storage object. I{dev} is the path to the TYPE_DISK 369 | block device to use. 370 | ''' 371 | self.assert_root() 372 | self.assert_available_so_name(name) 373 | backstore = IBlockBackstore(self.next_hba_index(), mode='create') 374 | try: 375 | so = IBlockStorageObject(backstore, name, dev) 376 | except Exception, exception: 377 | backstore.delete() 378 | raise exception 379 | ui_so = UIStorageObject(so, self) 380 | self.shell.log.info("Created iblock storage object %s using %s." 381 | % (name, dev)) 382 | return self.new_node(ui_so) 383 | 384 | 385 | class UIStorageObject(UIRTSLibNode): 386 | ''' 387 | A storage object UI. 388 | Abstract Base Class, do not instantiate. 389 | ''' 390 | def __init__(self, storage_object, parent): 391 | name = storage_object.name 392 | UIRTSLibNode.__init__(self, name, storage_object, parent) 393 | self.cfs_cwd = storage_object.path 394 | self.refresh() 395 | 396 | def ui_command_version(self): 397 | ''' 398 | Displays the version of the current backstore's plugin. 399 | ''' 400 | backstore = self.rtsnode.backstore 401 | self.shell.con.display("Backstore plugin %s %s" 402 | % (backstore.plugin, backstore.version)) 403 | 404 | def summary(self): 405 | so = self.rtsnode 406 | errors = [] 407 | if so.backstore.plugin.startswith("rd"): 408 | path = "ramdisk" 409 | else: 410 | path = so.udev_path 411 | 412 | if not path: 413 | errors.append("BROKEN STORAGE LINK") 414 | 415 | legacy = [] 416 | if self.rtsnode.name != self.name: 417 | legacy.append("ADDED SUFFIX") 418 | if len(list(self.rtsnode.backstore.storage_objects)) > 1: 419 | legacy.append("SHARED HBA") 420 | 421 | if legacy: 422 | errors.append("LEGACY: " + ", ".join(legacy)) 423 | 424 | size = convert_bytes_to_human(getattr(so, "size", 0)) 425 | if so.status == "activated": 426 | status = "in use" 427 | else: 428 | status = "not in use" 429 | nullio_str = "" 430 | try: 431 | if so.nullio: 432 | nullio_str = "nullio" 433 | except AttributeError: 434 | pass 435 | 436 | if errors: 437 | info = ", ".join(errors) 438 | if path: 439 | info += " (%s %s)" % (path, status) 440 | return (info, False) 441 | else: 442 | info = ", ".join(["%s" % str(data) 443 | for data in (size, path, status, nullio_str) 444 | if data]) 445 | return (info, True) 446 | -------------------------------------------------------------------------------- /targetcli/ui_backstore_legacy.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Implements the targetcli backstores related UI. 3 | 4 | his file is part of targetcli. 5 | Copyright (c) 2011-2014 by Datera, Inc 6 | 7 | Licensed under the Apache License, Version 2.0 (the "License"); you may 8 | not use this file except in compliance with the License. You may obtain 9 | a copy of the License at 10 | 11 | http://www.apache.org/licenses/LICENSE-2.0 12 | 13 | Unless required by applicable law or agreed to in writing, software 14 | distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 15 | WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 16 | License for the specific language governing permissions and limitations 17 | under the License. 18 | ''' 19 | 20 | from ui_node import UINode, UIRTSLibNode 21 | from rtslib import RTSRoot 22 | from rtslib import FileIOBackstore, IBlockBackstore 23 | from rtslib import PSCSIBackstore, RDMCPBackstore 24 | from rtslib import FileIOStorageObject, IBlockStorageObject 25 | from rtslib import PSCSIStorageObject, RDMCPStorageObject 26 | from rtslib.utils import get_block_type, is_disk_partition 27 | 28 | class UIBackstoresLegacy(UINode): 29 | ''' 30 | The backstores container UI. 31 | ''' 32 | def __init__(self, parent): 33 | UINode.__init__(self, 'backstores', parent) 34 | self.cfs_cwd = "%s/core" % self.cfs_cwd 35 | self.refresh() 36 | 37 | def refresh(self): 38 | self._children = set([]) 39 | for backstore in RTSRoot().backstores: 40 | backstore_plugin = backstore.plugin 41 | if backstore_plugin == 'pscsi': 42 | UIPSCSIBackstoreLegacy(backstore, self) 43 | elif backstore_plugin == 'rd_mcp': 44 | UIRDMCPBackstoreLegacy(backstore, self) 45 | elif backstore_plugin == 'fileio': 46 | UIFileIOBackstoreLegacy(backstore, self) 47 | elif backstore_plugin == 'iblock': 48 | UIIBlockBackstoreLegacy(backstore, self) 49 | 50 | def summary(self): 51 | no_backstores = len(self._children) 52 | if no_backstores > 1: 53 | msg = "%d Backstores (legacy mode)" % no_backstores 54 | else: 55 | msg = "%d Backstore (legacy mode)" % no_backstores 56 | return (msg, None) 57 | 58 | def ui_command_create(self, backstore_plugin): 59 | ''' 60 | Creates a new backstore, using the chosen I{backstore_plugin}. More 61 | than one backstores using the same I{backstore_plugin} can co-exist. 62 | They will be identified by incremental index numbers, starting from 0. 63 | 64 | AVAILABLE BACKSTORE PLUGINS 65 | =========================== 66 | 67 | B{iblock} 68 | --------- 69 | This I{backstore_plugin} provides I{SPC-4}, along with I{ALUA} and 70 | I{Persistent Reservations} emulation on top of Linux BLOCK devices: 71 | B{any block device} that appears in /sys/block. 72 | 73 | B{pscsi} 74 | -------- 75 | Provides pass-through for Linux physical SCSI devices. It can be used 76 | with any storage object that does B{direct pass-through} of SCSI 77 | commands without SCSI emulation. This assumes an underlying SCSI 78 | device that appears with lsscsi in /proc/scsi/scsi, such as a SAS hard 79 | drive, such as any SCSI device. The Linux kernel code for device SCSI 80 | drivers resides in linux/drivers/scsi. SCSI-3 and higher is supported 81 | with this subsystem, but only for control CDBs capable by the device 82 | firmware. 83 | 84 | B{fileio} 85 | --------- 86 | This I{backstore_plugin} provides I{SPC-4}, along with I{ALUA} and 87 | I{Persistent Reservations} emulation on top of Linux VFS devices: 88 | B{any file on a mounted filesystem}. It may be backed by a file or an 89 | underlying real block device. FILEIO is using struct file to serve 90 | block I/O with various methods (synchronous or asynchronous) and 91 | (buffered or direct). 92 | 93 | B{rd_mcp} 94 | -------- 95 | This I{backstore_plugin} uses a ramdisk with a separate 96 | mapping using memory copy. Typically used for bandwidth 97 | testing. 98 | 99 | EXAMPLE 100 | ======= 101 | 102 | B{create iblock} 103 | ---------------- 104 | Creates a new backstore, using the B{iblock} I{backstore_plugin}. 105 | ''' 106 | self.assert_root() 107 | self.shell.log.debug("%r" % [(backstore.plugin, backstore.index) 108 | for backstore in RTSRoot().backstores]) 109 | indexes = [backstore.index for backstore in RTSRoot().backstores 110 | if backstore.plugin == backstore_plugin] 111 | self.shell.log.debug("Existing %s backstore indexes: %r" 112 | % (backstore_plugin, indexes)) 113 | for index in range(1048576): 114 | if index not in indexes: 115 | backstore_index = index 116 | break 117 | 118 | if backstore_index is None: 119 | self.shell.log.error("Cannot find an available backstore index.") 120 | return 121 | else: 122 | self.shell.log.info("First available %s backstore index is %d." 123 | % (backstore_plugin, backstore_index)) 124 | 125 | if backstore_plugin == 'pscsi': 126 | backstore = PSCSIBackstore(backstore_index, mode='create') 127 | return self.new_node(UIPSCSIBackstoreLegacy(backstore, self)) 128 | elif backstore_plugin == 'rd_mcp': 129 | backstore = RDMCPBackstore(backstore_index, mode='create') 130 | return self.new_node(UIRDMCPBackstoreLegacy(backstore, self)) 131 | elif backstore_plugin == 'fileio': 132 | backstore = FileIOBackstore(backstore_index, mode='create') 133 | return self.new_node(UIFileIOBackstoreLegacy(backstore, self)) 134 | elif backstore_plugin == 'iblock': 135 | backstore = IBlockBackstore(backstore_index, mode='create') 136 | return self.new_node(UIIBlockBackstoreLegacy(backstore, self)) 137 | else: 138 | self.shell.log.error("Invalid backstore plugin %s" 139 | % backstore_plugin) 140 | return 141 | 142 | self.shell.log.info("Created new backstore %s" % backstore.name) 143 | 144 | def ui_complete_create(self, parameters, text, current_param): 145 | ''' 146 | Parameter auto-completion method for user command create. 147 | @param parameters: Parameters on the command line. 148 | @type parameters: dict 149 | @param text: Current text of parameter being typed by the user. 150 | @type text: str 151 | @param current_param: Name of parameter to complete. 152 | @type current_param: str 153 | @return: Possible completions 154 | @rtype: list of str 155 | ''' 156 | if current_param == 'backstore_plugin': 157 | plugins = ['pscsi', 'rd_mcp', 'fileio', 'iblock'] 158 | completions = [plugin for plugin in plugins 159 | if plugin.startswith(text)] 160 | else: 161 | completions = [] 162 | 163 | if len(completions) == 1: 164 | return [completions[0] + ' '] 165 | else: 166 | return completions 167 | 168 | def ui_command_delete(self, backstore): 169 | ''' 170 | Deletes a I{backstore}, and recursively all defined storage objects 171 | hanging under it. If there are existing LUNs making use of those 172 | storage objects, they will be deleted too. 173 | 174 | EXAMPLE 175 | ======= 176 | B{delete iblock2} 177 | ----------------- 178 | That would recursively delete the B{iblock} backstore with index 2. 179 | ''' 180 | self.assert_root() 181 | try: 182 | child = self.get_child(backstore) 183 | except ValueError: 184 | self.shell.log.error("No backstore named %s." % backstore) 185 | else: 186 | child.rtsnode.delete() 187 | self.remove_child(child) 188 | self.shell.log.info("Deleted backstore %s." % backstore) 189 | self.parent.refresh() 190 | 191 | def ui_complete_delete(self, parameters, text, current_param): 192 | ''' 193 | Parameter auto-completion method for user command delete. 194 | @param parameters: Parameters on the command line. 195 | @type parameters: dict 196 | @param text: Current text of parameter being typed by the user. 197 | @type text: str 198 | @param current_param: Name of parameter to complete. 199 | @type current_param: str 200 | @return: Possible completions 201 | @rtype: list of str 202 | ''' 203 | if current_param == 'backstore': 204 | backstores = [child.name for child in self.children] 205 | completions = [backstore for backstore in backstores 206 | if backstore.startswith(text)] 207 | else: 208 | completions = [] 209 | 210 | if len(completions) == 1: 211 | return [completions[0] + ' '] 212 | else: 213 | return completions 214 | 215 | 216 | class UIBackstoreLegacy(UIRTSLibNode): 217 | ''' 218 | A backstore UI. 219 | ''' 220 | def __init__(self, backstore, parent): 221 | UIRTSLibNode.__init__(self, backstore.name, backstore, parent) 222 | self.cfs_cwd = backstore.path 223 | self.refresh() 224 | 225 | def refresh(self): 226 | self._children = set([]) 227 | for storage_object in self.rtsnode.storage_objects: 228 | UIStorageObjectLegacy(storage_object, self) 229 | 230 | def summary(self): 231 | no_storage_objects = len(self._children) 232 | if no_storage_objects > 1: 233 | msg = "%d Storage Objects" % no_storage_objects 234 | else: 235 | msg = "%d Storage Object" % no_storage_objects 236 | return (msg, None) 237 | 238 | def prm_buffered(self, buffered): 239 | buffered = \ 240 | self.ui_eval_param(buffered, 'bool', True) 241 | if buffered: 242 | self.shell.log.info("Using buffered mode.") 243 | else: 244 | self.shell.log.info("Not using buffered mode.") 245 | return buffered 246 | 247 | def ui_command_version(self): 248 | ''' 249 | Displays the version of the current backstore's plugin. 250 | ''' 251 | self.shell.con.display("Backstore plugin %s %s" 252 | % (self.rtsnode.plugin, self.rtsnode.version)) 253 | 254 | def ui_command_delete(self, name): 255 | ''' 256 | Recursively deletes the storage object having the specified I{name}. If 257 | there are LUNs using this storage object, they will be deleted too. 258 | 259 | EXAMPLE 260 | ======= 261 | B{delete mystorage} 262 | ------------------- 263 | Deletes the storage object named mystorage, and all associated LUNs. 264 | ''' 265 | self.assert_root() 266 | try: 267 | child = self.get_child(name) 268 | except ValueError: 269 | self.shell.log.error("No storage object named %s." % name) 270 | else: 271 | child.rtsnode.delete() 272 | self.remove_child(child) 273 | self.shell.log.info("Deleted storage object %s." % name) 274 | self.parent.parent.refresh() 275 | 276 | def ui_complete_delete(self, parameters, text, current_param): 277 | ''' 278 | Parameter auto-completion method for user command delete. 279 | @param parameters: Parameters on the command line. 280 | @type parameters: dict 281 | @param text: Current text of parameter being typed by the user. 282 | @type text: str 283 | @param current_param: Name of parameter to complete. 284 | @type current_param: str 285 | @return: Possible completions 286 | @rtype: list of str 287 | ''' 288 | if current_param == 'name': 289 | names = [child.name for child in self.children] 290 | completions = [name for name in names 291 | if name.startswith(text)] 292 | else: 293 | completions = [] 294 | 295 | if len(completions) == 1: 296 | return [completions[0] + ' '] 297 | else: 298 | return completions 299 | 300 | 301 | class UIPSCSIBackstoreLegacy(UIBackstoreLegacy): 302 | ''' 303 | PSCSI backstore UI. 304 | ''' 305 | def ui_command_create(self, name, dev): 306 | ''' 307 | Creates a PSCSI storage object, with supplied name and SCSI device. The 308 | SCSI device I{dev} can either be a path name to the device, in which 309 | case it is recommended to use the /dev/disk/by-id hierarchy to have 310 | consistent naming should your physical SCSI system be modified, or an 311 | SCSI device ID in the H:C:T:L format, which is not recommended as SCSI 312 | IDs may vary in time. 313 | ''' 314 | self.assert_root() 315 | so = PSCSIStorageObject(self.rtsnode, name, dev) 316 | ui_so = UIStorageObjectLegacy(so, self) 317 | self.shell.log.info("Created pscsi storage object %s using %s." 318 | % (name, dev)) 319 | return self.new_node(ui_so) 320 | 321 | class UIRDMCPBackstoreLegacy(UIBackstoreLegacy): 322 | ''' 323 | RDMCP backstore UI. 324 | ''' 325 | def ui_command_create(self, name, size): 326 | ''' 327 | Creates an RDMCP storage object. I{size} is the size of the ramdisk. 328 | 329 | SIZE SYNTAX 330 | =========== 331 | - If size is an int, it represents a number of bytes. 332 | - If size is a string, the following units can be used: 333 | - B{B} or no unit present for bytes 334 | - B{k}, B{K}, B{kB}, B{KB} for kB (kilobytes) 335 | - B{m}, B{M}, B{mB}, B{MB} for MB (megabytes) 336 | - B{g}, B{G}, B{gB}, B{GB} for GB (gigabytes) 337 | - B{t}, B{T}, B{tB}, B{TB} for TB (terabytes) 338 | ''' 339 | self.assert_root() 340 | so = RDMCPStorageObject(self.rtsnode, name, size) 341 | ui_so = UIStorageObjectLegacy(so, self) 342 | self.shell.log.info("Created rd_mcp ramdisk %s with size %s." 343 | % (name, size)) 344 | return self.new_node(ui_so) 345 | 346 | 347 | class UIFileIOBackstoreLegacy(UIBackstoreLegacy): 348 | ''' 349 | FileIO backstore UI. 350 | ''' 351 | def ui_command_create(self, name, file_or_dev, size=None, buffered=None): 352 | ''' 353 | Creates a FileIO storage object. If I{file_or_dev} is a path to a 354 | regular file to be used as backend, then the I{size} parameter is 355 | mandatory. Else, if I{file_or_dev} is a path to a block device, the 356 | size parameter B{must} be ommited. If present, I{size} is the size of 357 | the file to be used, I{file} the path to the file or I{dev} the path to 358 | a block device. The I{buffered} parameter is a boolean stating 359 | whether or not to enable buffered mode. It is disabled by 360 | default (synchronous mode). 361 | 362 | SIZE SYNTAX 363 | =========== 364 | - If size is an int, it represents a number of bytes. 365 | - If size is a string, the following units can be used: 366 | - B{B} or no unit present for bytes 367 | - B{k}, B{K}, B{kB}, B{KB} for kB (kilobytes) 368 | - B{m}, B{M}, B{mB}, B{MB} for MB (megabytes) 369 | - B{g}, B{G}, B{gB}, B{GB} for GB (gigabytes) 370 | - B{t}, B{T}, B{tB}, B{TB} for TB (terabytes) 371 | ''' 372 | self.assert_root() 373 | self.shell.log.debug('Using params size=%s buffered=%s' 374 | % (size, buffered)) 375 | is_dev = get_block_type(file_or_dev) is not None \ 376 | or is_disk_partition(file_or_dev) 377 | 378 | if size is None and is_dev: 379 | so = FileIOStorageObject(self.rtsnode, name, file_or_dev, 380 | buffered_mode=self.prm_buffered(buffered)) 381 | self.shell.log.info("Created fileio %s with size %s." 382 | % (name, size)) 383 | ui_so = UIStorageObjectLegacy(so, self) 384 | return self.new_node(ui_so) 385 | elif size is not None and not is_dev: 386 | so = FileIOStorageObject(self.rtsnode, name, file_or_dev, size, 387 | buffered_mode=self.prm_buffered(buffered)) 388 | self.shell.log.info("Created fileio storage object %s." % name) 389 | ui_so = UIStorageObjectLegacy(so, self) 390 | return self.new_node(ui_so) 391 | else: 392 | self.shell.log.error("For fileio, you must either specify both a " 393 | + "file and a size, or just a device path.") 394 | 395 | 396 | class UIIBlockBackstoreLegacy(UIBackstoreLegacy): 397 | ''' 398 | IBlock backstore UI. 399 | ''' 400 | def ui_command_create(self, name, dev): 401 | ''' 402 | Creates an IBlock Storage object. I{dev} is the path to the TYPE_DISK 403 | block device to use. 404 | ''' 405 | self.assert_root() 406 | so = IBlockStorageObject(self.rtsnode, name, dev) 407 | ui_so = UIStorageObjectLegacy(so, self) 408 | self.shell.log.info("Created iblock storage object %s using %s." 409 | % (name, dev)) 410 | return self.new_node(ui_so) 411 | 412 | 413 | class UIStorageObjectLegacy(UIRTSLibNode): 414 | ''' 415 | A storage object UI. 416 | ''' 417 | def __init__(self, storage_object, parent): 418 | name = storage_object.name 419 | UIRTSLibNode.__init__(self, name, storage_object, parent) 420 | self.cfs_cwd = storage_object.path 421 | self.refresh() 422 | 423 | def summary(self): 424 | so = self.rtsnode 425 | if so.backstore.plugin.startswith("rd"): 426 | path = "ramdisk" 427 | else: 428 | path = so.udev_path 429 | if not path: 430 | return ("BROKEN STORAGE LINK", False) 431 | else: 432 | return ("%s %s" % (path, so.status), True) 433 | 434 | -------------------------------------------------------------------------------- /targetcli/ui_node.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Implements the targetcli base UI node. 3 | 4 | This file is part of LIO(tm). 5 | Copyright (c) 2011-2014 by Datera, Inc 6 | 7 | Licensed under the Apache License, Version 2.0 (the "License"); you may 8 | not use this file except in compliance with the License. You may obtain 9 | a copy of the License at 10 | 11 | http://www.apache.org/licenses/LICENSE-2.0 12 | 13 | Unless required by applicable law or agreed to in writing, software 14 | distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 15 | WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 16 | License for the specific language governing permissions and limitations 17 | under the License. 18 | ''' 19 | 20 | from configshell import ConfigNode, ExecutionError 21 | from rtslib import RTSLibError, RTSRoot, Config 22 | from subprocess import PIPE, Popen 23 | from cli_config import CliConfig 24 | from os.path import isfile 25 | from os import getuid 26 | 27 | STARTUP_CONFIG = "/etc/target/scsi_target.lio" 28 | 29 | def exec3(cmd): 30 | ''' 31 | Executes a shell command **cmd** and returns 32 | **(retcode, stdout, stderr)**. 33 | ''' 34 | process = Popen(cmd, shell=True, bufsize=1024*1024, 35 | stdin=PIPE, 36 | stdout=PIPE, stderr=PIPE, 37 | close_fds=True) 38 | (out, err) = process.communicate() 39 | retcode = process.returncode 40 | return (retcode, out, err) 41 | 42 | class UINode(ConfigNode): 43 | ''' 44 | Our targetcli basic UI node. 45 | ''' 46 | def __init__(self, name, parent=None, shell=None): 47 | ConfigNode.__init__(self, name, parent, shell) 48 | self.cfs_cwd = RTSRoot.configfs_dir 49 | self.define_config_group_param( 50 | 'global', 'auto_enable_tpg', 'bool', 51 | 'If true, automatically enables TPGs upon creation.') 52 | self.define_config_group_param( 53 | 'global', 'auto_add_mapped_luns', 'bool', 54 | 'If true, automatically create node ACLs mapped LUNs ' 55 | + 'after creating a new target LUN or a new node ACL') 56 | self.define_config_group_param( 57 | 'global', 'legacy_hba_view', 'bool', 58 | 'If true, use legacy HBA view, allowing to create more ' 59 | + 'than one storage object per HBA.') 60 | self.define_config_group_param( 61 | 'global', 'auto_cd_after_create', 'bool', 62 | 'If true, changes current path to newly created objects.') 63 | 64 | def assert_root(self): 65 | ''' 66 | For commands requiring root privileges, disable command if not the root 67 | node's as_root attribute is False. 68 | ''' 69 | root_node = self.get_root() 70 | if hasattr(root_node, 'as_root') and not root_node.as_root: 71 | raise ExecutionError("This privileged command is disabled: " 72 | + "you are not root.") 73 | 74 | def new_node(self, new_node): 75 | ''' 76 | Used to honor global 'auto_cd_after_create'. 77 | Either returns None if the global is False, or the new_node if the 78 | global is True. In both cases, set the @last bookmark to last_node. 79 | ''' 80 | self.shell.prefs['bookmarks']['last'] = new_node.path 81 | self.shell.prefs.save() 82 | if self.shell.prefs['auto_cd_after_create']: 83 | self.shell.log.info("Entering new node %s" % new_node.path) 84 | # Piggy backs on cd instead of just returning new_node, 85 | # so we update navigation history. 86 | return self.ui_command_cd(new_node.path) 87 | else: 88 | return None 89 | 90 | def refresh(self): 91 | ''' 92 | Refreshes and updates the objects tree from the current path. 93 | ''' 94 | for child in self.children: 95 | child.refresh() 96 | 97 | def execute_command(self, command, pparams=[], kparams={}): 98 | ''' 99 | We overload this one in order to handle our own exceptions cleanly, 100 | and not just configshell's ExecutionError. 101 | ''' 102 | try: 103 | result = ConfigNode.execute_command(self, command, 104 | pparams, kparams) 105 | except RTSLibError, msg: 106 | self.shell.log.error(str(msg)) 107 | else: 108 | self.shell.log.debug("Command %s succeeded." % command) 109 | return result 110 | 111 | def ui_command_saveconfig(self): 112 | ''' 113 | Saves the whole configuration tree to disk so that it will be restored 114 | on next boot. Unless you do that, changes are lost accross reboots. 115 | ''' 116 | self.assert_root() 117 | try: 118 | input = raw_input("Save configuration? [Y/n]: ") 119 | except EOFError: 120 | input = None 121 | self.shell.con.display('') 122 | if input in ["y", "Y", ""]: 123 | CliConfig.save_running_config() 124 | else: 125 | self.shell.log.warning("Configuration not saved.") 126 | 127 | def ui_command_exit(self): 128 | ''' 129 | Exits the command line interface. 130 | ''' 131 | if getuid() == 0: 132 | self.shell.log.info("Comparing startup and running configs...") 133 | try: 134 | config = Config() 135 | if isfile(STARTUP_CONFIG): 136 | config.load(STARTUP_CONFIG, allow_new_attrs=True) 137 | saved_config = config.dump() 138 | config.load_live() 139 | live_config = config.dump() 140 | if saved_config != live_config: 141 | self.shell.log.info("Some changes need saving.") 142 | self.ui_command_saveconfig() 143 | else: 144 | self.shell.log.info("Startup config is up-to-date.") 145 | except Exception, e: 146 | self.shell.log.warning(e) 147 | 148 | return 'EXIT' 149 | 150 | def ui_command_refresh(self): 151 | ''' 152 | Refreshes and updates the objects tree from the current path. 153 | ''' 154 | self.refresh() 155 | 156 | def ui_command_status(self): 157 | ''' 158 | Displays the current node's status summary. 159 | 160 | SEE ALSO 161 | ======== 162 | B{ls} 163 | ''' 164 | description, is_healthy = self.summary() 165 | self.shell.log.info("Status for %s: %s" % (self.path, description)) 166 | 167 | def ui_setgroup_global(self, parameter, value): 168 | ConfigNode.ui_setgroup_global(self, parameter, value) 169 | self.get_root().refresh() 170 | 171 | 172 | class UIRTSLibNode(UINode): 173 | ''' 174 | A subclass of UINode for nodes with an underlying RTSLib object. 175 | ''' 176 | def __init__(self, name, rtslib_object, parent): 177 | ''' 178 | Call from the class that inherits this, with the rtslib object that 179 | should be checked upon. 180 | ''' 181 | UINode.__init__(self, name, parent) 182 | self.rtsnode = rtslib_object 183 | 184 | # If the rtsnode has parameters, use them 185 | parameters = self.rtsnode.list_parameters() 186 | parameters_ro = self.rtsnode.list_parameters(writable=False) 187 | for parameter in parameters: 188 | writable = parameter not in parameters_ro 189 | description = "The %s parameter." % parameter 190 | self.define_config_group_param( 191 | 'parameter', parameter, 'string', description, writable) 192 | 193 | # If the rtsnode has attributes, enable them 194 | attributes = self.rtsnode.list_attributes() 195 | attributes_ro = self.rtsnode.list_attributes(writable=False) 196 | for attribute in attributes: 197 | writable = attribute not in attributes_ro 198 | description = "The %s attribute." % attribute 199 | self.define_config_group_param( 200 | 'attribute', attribute, 'string', description, writable) 201 | 202 | # If the rtsnode has auth_attrs, use them 203 | auth_attrs = self.rtsnode.list_auth_attrs() 204 | auth_attrs_ro = self.rtsnode.list_auth_attrs(writable=False) 205 | for auth_attr in auth_attrs: 206 | writable = auth_attr not in auth_attrs_ro 207 | description = "The %s auth_attr." % auth_attr 208 | self.define_config_group_param( 209 | 'auth', auth_attr, 'string', description, writable) 210 | 211 | def execute_command(self, command, pparams=[], kparams={}): 212 | ''' 213 | Overrides the parent's execute_command() to check if the underlying 214 | RTSLib object still exists before returning. 215 | ''' 216 | try: 217 | self.rtsnode._check_self() 218 | except RTSLibError: 219 | self.shell.log.error("The underlying rtslib object for " 220 | + "%s does not exist." % self.path) 221 | root = self.get_root() 222 | root.refresh() 223 | return root 224 | 225 | return UINode.execute_command(self, command, pparams, kparams) 226 | 227 | def ui_getgroup_attribute(self, attribute): 228 | ''' 229 | This is the backend method for getting attributes. 230 | @param attribute: The attribute to get the value of. 231 | @type attribute: str 232 | @return: The attribute's value 233 | @rtype: arbitrary 234 | ''' 235 | return self.rtsnode.get_attribute(attribute) 236 | 237 | def ui_setgroup_attribute(self, attribute, value): 238 | ''' 239 | This is the backend method for setting attributes. 240 | @param attribute: The attribute to set the value of. 241 | @type attribute: str 242 | @param value: The attribute's value 243 | @type value: arbitrary 244 | ''' 245 | self.assert_root() 246 | self.rtsnode.set_attribute(attribute, value) 247 | 248 | def ui_getgroup_parameter(self, parameter): 249 | ''' 250 | This is the backend method for getting parameters. 251 | @param parameter: The parameter to get the value of. 252 | @type parameter: str 253 | @return: The parameter's value 254 | @rtype: arbitrary 255 | ''' 256 | return self.rtsnode.get_parameter(parameter) 257 | 258 | def ui_setgroup_parameter(self, parameter, value): 259 | ''' 260 | This is the backend method for setting parameters. 261 | @param parameter: The parameter to set the value of. 262 | @type parameter: str 263 | @param value: The parameter's value 264 | @type value: arbitrary 265 | ''' 266 | self.assert_root() 267 | self.rtsnode.set_parameter(parameter, value) 268 | 269 | def ui_getgroup_auth(self, auth_attr): 270 | ''' 271 | This is the backend method for getting auth_attrs. 272 | @param auth_attr: The auth_attr to get the value of. 273 | @type auth_attr: str 274 | @return: The auth_attr's value 275 | @rtype: arbitrary 276 | ''' 277 | return self.rtsnode.get_auth_attr(auth_attr) 278 | 279 | def ui_setgroup_auth(self, auth_attr, value): 280 | ''' 281 | This is the backend method for setting auth_attrs. 282 | @param auth_attr: The auth_attr to set the value of. 283 | @type auth_attr: str 284 | @param value: The auth_attr's value 285 | @type value: arbitrary 286 | ''' 287 | self.assert_root() 288 | self.rtsnode.set_auth_attr(auth_attr, value) 289 | 290 | 291 | -------------------------------------------------------------------------------- /targetcli/ui_root.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Implements the targetcli root UI. 3 | 4 | This file is part of LIO(tm). 5 | Copyright (c) 2011-2014 by Datera, Inc 6 | 7 | Licensed under the Apache License, Version 2.0 (the "License"); you may 8 | not use this file except in compliance with the License. You may obtain 9 | a copy of the License at 10 | 11 | http://www.apache.org/licenses/LICENSE-2.0 12 | 13 | Unless required by applicable law or agreed to in writing, software 14 | distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 15 | WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 16 | License for the specific language governing permissions and limitations 17 | under the License. 18 | ''' 19 | from os import system 20 | import readline, tempfile 21 | from rtslib import RTSRoot, Config 22 | from ui_node import UINode, STARTUP_CONFIG 23 | from ui_target import UIFabricModule 24 | from ui_backstore import UIBackstores 25 | from ui_backstore_legacy import UIBackstoresLegacy 26 | 27 | class UIRoot(UINode): 28 | ''' 29 | The targetcli hierarchy root node. 30 | ''' 31 | def __init__(self, shell, as_root=False): 32 | UINode.__init__(self, '/', shell=shell) 33 | self.as_root = as_root 34 | 35 | def refresh(self): 36 | ''' 37 | Refreshes the tree of target fabric modules. 38 | ''' 39 | self._children = set([]) 40 | if self.shell.prefs['legacy_hba_view']: 41 | UIBackstoresLegacy(self) 42 | else: 43 | UIBackstores(self) 44 | 45 | for fabric_module in RTSRoot().fabric_modules: 46 | self.shell.log.debug("Using fabric module %s." % fabric_module.name) 47 | UIFabricModule(fabric_module, self) 48 | 49 | def ui_command_configure(self): 50 | ''' 51 | Enters the config mode. 52 | 53 | This mode allows editing a candidate configuration without 54 | impacting the running system. This candidate configuration can 55 | then either be commited or discarded at will. If commited, it 56 | will be applied to the running system and saved as the new 57 | startup configuration. 58 | 59 | Other features include loading a configuration from file, undo 60 | support, rollback support, configuration backups and more. 61 | 62 | This mode is a functionnal but early preview version of the next- 63 | generation targetcli environment. 64 | ''' 65 | self.assert_root() 66 | self.shell.log.warning("Entering configure mode") 67 | self.shell.log.warning("This mode is a functionnal but early " 68 | "preview version of the next-generation " 69 | "targetcli") 70 | system("targetcli-ng configure") 71 | self.refresh() 72 | 73 | def ui_command_version(self): 74 | ''' 75 | Displays the targetcli and support libraries versions. 76 | ''' 77 | from rtslib import __version__ as rtslib_version 78 | from targetcli import __version__ as targetcli_version 79 | from configshell import __version__ as configshell_version 80 | for package, version in dict(targetcli=targetcli_version, 81 | rtslib=rtslib_version, 82 | configshell=configshell_version).items(): 83 | if version == 'GIT_VERSION': 84 | self.shell.log.error("Cannot find %s version. The %s package " 85 | % (package, package) 86 | + "has probably not been built properly " 87 | + "from either the git repository or a " 88 | + "public tarball.") 89 | else: 90 | self.shell.log.info("Using %s version %s" % (package, version)) 91 | 92 | -------------------------------------------------------------------------------- /targetcli/ui_target.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Implements the targetcli target related UI. 3 | 4 | This file is part of LIO(tm). 5 | Copyright (c) 2011-2014 by Datera, Inc 6 | 7 | Licensed under the Apache License, Version 2.0 (the "License"); you may 8 | not use this file except in compliance with the License. You may obtain 9 | a copy of the License at 10 | 11 | http://www.apache.org/licenses/LICENSE-2.0 12 | 13 | Unless required by applicable law or agreed to in writing, software 14 | distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 15 | WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 16 | License for the specific language governing permissions and limitations 17 | under the License. 18 | ''' 19 | 20 | from ui_node import UINode, UIRTSLibNode 21 | from ui_backstore import dedup_so_name 22 | from rtslib import RTSLibError, RTSLibBrokenLink, utils 23 | from rtslib import NodeACL, NetworkPortal, MappedLUN 24 | from rtslib import Target, TPG, LUN 25 | 26 | class UIFabricModule(UIRTSLibNode): 27 | ''' 28 | A fabric module UI. 29 | ''' 30 | def __init__(self, fabric_module, parent): 31 | UIRTSLibNode.__init__(self, fabric_module.name, fabric_module, parent) 32 | self.cfs_cwd = fabric_module.path 33 | self.refresh() 34 | if self.rtsnode.has_feature('discovery_auth'): 35 | for param in ['userid', 'password', 36 | 'mutual_userid', 'mutual_password', 37 | 'enable']: 38 | self.define_config_group_param('discovery_auth', 39 | param, 'string') 40 | self.refresh() 41 | 42 | def ui_getgroup_discovery_auth(self, auth_attr): 43 | ''' 44 | This is the backend method for getting discovery_auth attributes. 45 | @param auth_attr: The auth attribute to get the value of. 46 | @type auth_attr: str 47 | @return: The auth attribute's value 48 | @rtype: str 49 | ''' 50 | value = None 51 | if auth_attr == 'password': 52 | value = self.rtsnode.discovery_password 53 | elif auth_attr == 'userid': 54 | value = self.rtsnode.discovery_userid 55 | elif auth_attr == 'mutual_password': 56 | value = self.rtsnode.discovery_mutual_password 57 | elif auth_attr == 'mutual_userid': 58 | value = self.rtsnode.discovery_mutual_userid 59 | elif auth_attr == 'enable': 60 | value = self.rtsnode.discovery_enable_auth 61 | return value 62 | 63 | def ui_setgroup_discovery_auth(self, auth_attr, value): 64 | ''' 65 | This is the backend method for setting discovery auth attributes. 66 | @param auth_attr: The auth attribute to set the value of. 67 | @type auth_attr: str 68 | @param value: The auth's value 69 | @type value: str 70 | ''' 71 | self.assert_root() 72 | if value is None: 73 | value = '' 74 | if auth_attr == 'password': 75 | self.rtsnode.discovery_password = value 76 | elif auth_attr == 'userid': 77 | self.rtsnode.discovery_userid = value 78 | elif auth_attr == 'mutual_password': 79 | self.rtsnode.discovery_mutual_password = value 80 | elif auth_attr == 'mutual_userid': 81 | self.rtsnode.discovery_mutual_userid = value 82 | elif auth_attr == 'enable': 83 | self.rtsnode.discovery_enable_auth = value 84 | 85 | def refresh(self): 86 | self._children = set([]) 87 | for target in self.rtsnode.targets: 88 | self.shell.log.debug("Found target %s under fabric module %s." 89 | % (target.wwn, target.fabric_module)) 90 | if target.has_feature('tpgts'): 91 | UIMultiTPGTarget(target, self) 92 | else: 93 | UITarget(target, self) 94 | 95 | def summary(self): 96 | no_targets = len(self._children) 97 | if no_targets != 1: 98 | msg = "%d Targets" % no_targets 99 | else: 100 | msg = "%d Target" % no_targets 101 | return (msg, None) 102 | 103 | def ui_command_create(self, wwn=None): 104 | ''' 105 | Creates a new target. The I{wwn} format depends on the transport(s) 106 | supported by the fabric module. If the I{wwn} is ommited, then a 107 | target will be created using either a randomly generated WWN of the 108 | proper type, or the first unused WWN in the list of possible WWNs if 109 | one is available. If WWNs are constrained to a list (i.e. for hardware 110 | targets addresses) and all WWNs are in use, the target creation will 111 | fail. Use the B{info} command to get more information abour WWN type 112 | and possible values. 113 | 114 | SEE ALSO 115 | ======== 116 | B{info} 117 | ''' 118 | self.assert_root() 119 | target = Target(self.rtsnode, wwn, mode='create') 120 | wwn = target.wwn 121 | if target.has_feature('tpgts'): 122 | ui_target = UIMultiTPGTarget(target, self) 123 | self.shell.log.info("Created target %s." % wwn) 124 | return ui_target.ui_command_create() 125 | else: 126 | ui_target = UITarget(target, self) 127 | self.shell.log.info("Created target %s." % wwn) 128 | return self.new_node(ui_target) 129 | 130 | def ui_complete_create(self, parameters, text, current_param): 131 | ''' 132 | Parameter auto-completion method for user command create. 133 | @param parameters: Parameters on the command line. 134 | @type parameters: dict 135 | @param text: Current text of parameter being typed by the user. 136 | @type text: str 137 | @param current_param: Name of parameter to complete. 138 | @type current_param: str 139 | @return: Possible completions 140 | @rtype: list of str 141 | ''' 142 | spec = self.rtsnode.spec 143 | if current_param == 'wwn' and spec['wwn_list'] is not None: 144 | existing_wwns = [child.wwn for child in self.rtsnode.targets] 145 | completions = [wwn for wwn in spec['wwn_list'] 146 | if wwn.startswith(text) 147 | if wwn not in existing_wwns] 148 | else: 149 | completions = [] 150 | 151 | if len(completions) == 1: 152 | return [completions[0] + ' '] 153 | else: 154 | return completions 155 | 156 | def ui_command_delete(self, wwn): 157 | ''' 158 | Recursively deletes the target with the specified I{wwn}, and all 159 | objects hanging under it. 160 | 161 | SEE ALSO 162 | ======== 163 | B{create} 164 | ''' 165 | self.assert_root() 166 | target = Target(self.rtsnode, wwn, mode='lookup') 167 | target.delete() 168 | self.shell.log.info("Deleted Target %s." % wwn) 169 | self.refresh() 170 | 171 | def ui_complete_delete(self, parameters, text, current_param): 172 | ''' 173 | Parameter auto-completion method for user command delete. 174 | @param parameters: Parameters on the command line. 175 | @type parameters: dict 176 | @param text: Current text of parameter being typed by the user. 177 | @type text: str 178 | @param current_param: Name of parameter to complete. 179 | @type current_param: str 180 | @return: Possible completions 181 | @rtype: list of str 182 | ''' 183 | if current_param == 'wwn': 184 | wwns = [child.name for child in self.children] 185 | completions = [wwn for wwn in wwns if wwn.startswith(text)] 186 | else: 187 | completions = [] 188 | 189 | if len(completions) == 1: 190 | return [completions[0] + ' '] 191 | else: 192 | return completions 193 | 194 | def ui_command_info(self): 195 | ''' 196 | Displays information about the fabric module, notably the supported 197 | transports(s) and accepted B{wwn} format(s), as long as supported 198 | features. 199 | ''' 200 | spec = self.rtsnode.spec 201 | self.shell.log.info("Fabric module name: %s" % self.name) 202 | self.shell.log.info("ConfigFS path: %s" % self.rtsnode.path) 203 | if spec['wwn_list'] is not None: 204 | self.shell.log.info("Allowed WWNs list (%s type): %s" 205 | % (spec['wwn_type'], 206 | ', '.join(spec['wwn_list']))) 207 | else: 208 | self.shell.log.info("Supported WWN type: %s" % spec['wwn_type']) 209 | 210 | self.shell.log.info("Fabric module specfile: %s" 211 | % self.rtsnode.spec_file) 212 | self.shell.log.info("Fabric module features: %s" 213 | % ', '.join(spec['features'])) 214 | self.shell.log.info("Corresponding kernel module: %s" 215 | % spec['kernel_module']) 216 | 217 | def ui_command_version(self): 218 | ''' 219 | Displays the target fabric module version. 220 | ''' 221 | version = "Target fabric module %s: %s" \ 222 | % (self.rtsnode.name, self.rtsnode.version) 223 | self.shell.con.display(version.strip()) 224 | 225 | 226 | class UIMultiTPGTarget(UIRTSLibNode): 227 | ''' 228 | A generic target UI that has multiple TPGs. 229 | ''' 230 | def __init__(self, target, parent): 231 | UIRTSLibNode.__init__(self, target.wwn, target, parent) 232 | self.cfs_cwd = target.path 233 | self.refresh() 234 | 235 | def refresh(self): 236 | self._children = set([]) 237 | for tpg in self.rtsnode.tpgs: 238 | UITPG(tpg, self) 239 | 240 | def summary(self): 241 | if not self.rtsnode.fabric_module.is_valid_wwn(self.rtsnode.wwn): 242 | description = "INVALID WWN" 243 | is_healthy = False 244 | else: 245 | is_healthy = None 246 | no_tpgs = len(self._children) 247 | if no_tpgs != 1: 248 | description = "%d TPGs" % no_tpgs 249 | else: 250 | description = "%d TPG" % no_tpgs 251 | 252 | return (description, is_healthy) 253 | 254 | def ui_command_create(self, tag=None): 255 | ''' 256 | Creates a new Target Portal Group within the target. The I{tag} must be 257 | a strictly positive integer value. If omitted, the next available 258 | Target Portal Group Tag (TPG) will be used. 259 | 260 | SEE ALSO 261 | ======== 262 | B{delete} 263 | ''' 264 | self.assert_root() 265 | if tag is None: 266 | tags = [tpg.tag for tpg in self.rtsnode.tpgs] 267 | for index in range(1048576): 268 | if index not in tags and index > 0: 269 | tag = index 270 | break 271 | if tag is None: 272 | self.shell.log.error("Cannot find an available TPG Tag.") 273 | return 274 | else: 275 | self.shell.log.info("Selected TPG Tag %d." % tag) 276 | else: 277 | try: 278 | tag = int(tag) 279 | except ValueError: 280 | self.shell.log.error("The TPG Tag must be an integer value.") 281 | return 282 | else: 283 | if tag < 0: 284 | self.shell.log.error("The TPG Tag must be 0 or more.") 285 | return 286 | 287 | tpg = TPG(self.rtsnode, tag, mode='create') 288 | if self.shell.prefs['auto_enable_tpgt']: 289 | tpg.enable = True 290 | self.shell.log.info("Created TPG %s." % tpg.tag) 291 | ui_tpg = UITPG(tpg, self) 292 | return self.new_node(ui_tpg) 293 | 294 | def ui_command_delete(self, tag): 295 | ''' 296 | Deletes the Target Portal Group with TPG I{tag} from the target. The 297 | I{tag} must be a positive integer matching an existing TPG. 298 | 299 | SEE ALSO 300 | ======== 301 | B{create} 302 | ''' 303 | self.assert_root() 304 | if tag.startswith("tpg"): 305 | tag = tag[3:] 306 | tpg = TPG(self.rtsnode, int(tag), mode='lookup') 307 | tpg.delete() 308 | self.shell.log.info("Deleted TPG %s." % tag) 309 | self.refresh() 310 | 311 | def ui_complete_delete(self, parameters, text, current_param): 312 | ''' 313 | Parameter auto-completion method for user command delete. 314 | @param parameters: Parameters on the command line. 315 | @type parameters: dict 316 | @param text: Current text of parameter being typed by the user. 317 | @type text: str 318 | @param current_param: Name of parameter to complete. 319 | @type current_param: str 320 | @return: Possible completions 321 | @rtype: list of str 322 | ''' 323 | if current_param == 'tag': 324 | tags = [child.name[4:] for child in self.children] 325 | completions = [tag for tag in tags if tag.startswith(text)] 326 | else: 327 | completions = [] 328 | 329 | if len(completions) == 1: 330 | return [completions[0] + ' '] 331 | else: 332 | return completions 333 | 334 | 335 | class UITPG(UIRTSLibNode): 336 | ''' 337 | A generic TPG UI. 338 | ''' 339 | def __init__(self, tpg, parent): 340 | name = "tpg%d" % tpg.tag 341 | UIRTSLibNode.__init__(self, name, tpg, parent) 342 | self.cfs_cwd = tpg.path 343 | self.refresh() 344 | 345 | UILUNs(tpg, self) 346 | 347 | if tpg.has_feature('acls'): 348 | UINodeACLs(self.rtsnode, self) 349 | if tpg.has_feature('nps'): 350 | UIPortals(self.rtsnode, self) 351 | 352 | def summary(self): 353 | if self.rtsnode.has_feature('nexus'): 354 | description = ("nexus WWN %s" % self.rtsnode.nexus_wwn, True) 355 | elif self.rtsnode.enable: 356 | description = ("enabled", True) 357 | else: 358 | description = ("disabled", False) 359 | return description 360 | 361 | def ui_command_enable(self): 362 | ''' 363 | Enables the TPG. 364 | 365 | SEE ALSO 366 | ======== 367 | B{disable status} 368 | ''' 369 | self.assert_root() 370 | if self.rtsnode.enable: 371 | self.shell.log.info("The TPG is already enabled.") 372 | else: 373 | self.rtsnode.enable = True 374 | self.shell.log.info("The TPG has been enabled.") 375 | 376 | def ui_command_disable(self): 377 | ''' 378 | Disables the TPG. 379 | 380 | SEE ALSO 381 | ======== 382 | B{enable status} 383 | ''' 384 | self.assert_root() 385 | if self.rtsnode.enable: 386 | self.rtsnode.enable = False 387 | self.shell.log.info("The TPG has been disabled.") 388 | else: 389 | self.shell.log.info("The TPG is already disabled.") 390 | 391 | 392 | class UITarget(UITPG): 393 | ''' 394 | A generic target UI merged with its only TPG. 395 | ''' 396 | def __init__(self, target, parent): 397 | UITPG.__init__(self, TPG(target, 1), parent) 398 | self._name = target.wwn 399 | self.target = target 400 | self.rtsnode.enable = True 401 | 402 | def summary(self): 403 | if not self.target.fabric_module.is_valid_wwn(self.target.wwn): 404 | return ("INVALID WWN", False) 405 | else: 406 | return UITPG.summary(self) 407 | 408 | 409 | class UINodeACLs(UINode): 410 | ''' 411 | A generic UI for node ACLs. 412 | ''' 413 | def __init__(self, tpg, parent): 414 | UINode.__init__(self, "acls", parent) 415 | self.tpg = tpg 416 | self.cfs_cwd = "%s/acls" % tpg.path 417 | self.refresh() 418 | 419 | def refresh(self): 420 | self._children = set([]) 421 | for node_acl in self.tpg.node_acls: 422 | UINodeACL(node_acl, self) 423 | 424 | def summary(self): 425 | no_acls = len(self._children) 426 | if no_acls != 1: 427 | msg = "%d ACLs" % no_acls 428 | else: 429 | msg = "%d ACL" % no_acls 430 | return (msg, None) 431 | 432 | def ui_command_create(self, wwn, add_mapped_luns=None): 433 | ''' 434 | Creates a Node ACL for the initiator node with the specified I{wwn}. 435 | The node's I{wwn} must match the expected WWN Type of the target's 436 | fabric module. 437 | 438 | If I{add_mapped_luns} is omitted, the global parameter 439 | B{auto_add_mapped_luns} will be used, else B{true} or B{false} are 440 | accepted. If B{true}, then after creating the ACL, mapped LUNs will be 441 | automatically created for all existing LUNs. 442 | 443 | SEE ALSO 444 | ======== 445 | B{delete} 446 | ''' 447 | self.assert_root() 448 | spec = self.tpg.parent_target.fabric_module.spec 449 | if not utils.is_valid_wwn(spec['wwn_type'], wwn): 450 | self.shell.log.error("'%s' is not a valid %s WWN." 451 | % (wwn, spec['wwn_type'])) 452 | return 453 | 454 | add_mapped_luns = \ 455 | self.ui_eval_param(add_mapped_luns, 'bool', 456 | self.shell.prefs['auto_add_mapped_luns']) 457 | 458 | try: 459 | node_acl = NodeACL(self.tpg, wwn, mode="create") 460 | except RTSLibError, msg: 461 | self.shell.log.error(str(msg)) 462 | return 463 | else: 464 | self.shell.log.info("Created Node ACL for %s" 465 | % node_acl.node_wwn) 466 | ui_node_acl = UINodeACL(node_acl, self) 467 | 468 | if add_mapped_luns: 469 | for lun in self.tpg.luns: 470 | MappedLUN(node_acl, lun.lun, lun.lun, write_protect=False) 471 | self.shell.log.info("Created mapped LUN %d." % lun.lun) 472 | self.refresh() 473 | 474 | return self.new_node(ui_node_acl) 475 | 476 | def ui_command_delete(self, wwn): 477 | ''' 478 | Deletes the Node ACL with the specified I{wwn}. 479 | 480 | SEE ALSO 481 | ======== 482 | B{create} 483 | ''' 484 | self.assert_root() 485 | node_acl = NodeACL(self.tpg, wwn, mode='lookup') 486 | node_acl.delete() 487 | self.shell.log.info("Deleted Node ACL %s." % wwn) 488 | self.refresh() 489 | 490 | def ui_complete_delete(self, parameters, text, current_param): 491 | ''' 492 | Parameter auto-completion method for user command delete. 493 | @param parameters: Parameters on the command line. 494 | @type parameters: dict 495 | @param text: Current text of parameter being typed by the user. 496 | @type text: str 497 | @param current_param: Name of parameter to complete. 498 | @type current_param: str 499 | @return: Possible completions 500 | @rtype: list of str 501 | ''' 502 | if current_param == 'wwn': 503 | wwns = [acl.node_wwn for acl in self.tpg.node_acls] 504 | completions = [wwn for wwn in wwns if wwn.startswith(text)] 505 | else: 506 | completions = [] 507 | 508 | if len(completions) == 1: 509 | return [completions[0] + ' '] 510 | else: 511 | return completions 512 | 513 | 514 | class UINodeACL(UIRTSLibNode): 515 | ''' 516 | A generic UI for a node ACL. 517 | ''' 518 | def __init__(self, node_acl, parent): 519 | UIRTSLibNode.__init__(self, node_acl.node_wwn, node_acl, parent) 520 | if self.rtsnode.has_feature("acls_tcq_depth"): 521 | self.define_config_group_param( 522 | 'attribute', 'tcq_depth', 'string', "Command queue depth.", True) 523 | self.cfs_cwd = node_acl.path 524 | self.refresh() 525 | 526 | def ui_getgroup_attribute(self, attribute): 527 | ''' 528 | This is the backend method for getting attributes. 529 | @param attribute: The attribute to get the value of. 530 | @type attribute: str 531 | @return: The attribute's value 532 | @rtype: arbitrary 533 | ''' 534 | if attribute == 'tcq_depth' and self.rtsnode.has_feature("acls_tcq_depth"): 535 | return self.rtsnode.tcq_depth 536 | else: 537 | return self.rtsnode.get_attribute(attribute) 538 | 539 | def ui_setgroup_attribute(self, attribute, value): 540 | ''' 541 | This is the backend method for setting attributes. 542 | @param attribute: The attribute to set the value of. 543 | @type attribute: str 544 | @param value: The attribute's value 545 | @type value: arbitrary 546 | ''' 547 | self.assert_root() 548 | if attribute == 'tcq_depth' and self.rtsnode.has_feature("acls_tcq_depth"): 549 | self.rtsnode.tcq_depth = value 550 | else: 551 | self.rtsnode.set_attribute(attribute, value) 552 | 553 | def refresh(self): 554 | self._children = set([]) 555 | for mlun in self.rtsnode.mapped_luns: 556 | UIMappedLUN(mlun, self) 557 | 558 | def summary(self): 559 | no_mluns = len(self._children) 560 | if no_mluns != 1: 561 | msg = "%d Mapped LUNs" % no_mluns 562 | else: 563 | msg = "%d Mapped LUN" % no_mluns 564 | return (msg, None) 565 | 566 | def ui_command_create(self, mapped_lun, tpg_lun, write_protect=None): 567 | ''' 568 | Creates a mapping to one of the TPG LUNs for the initiator referenced 569 | by the ACL. The provided I{tpg_lun} will appear to that initiator as 570 | LUN I{mapped_lun}. If the I{write_protect} flag is set to B{1}, the 571 | initiator will not have write access to the Mapped LUN. 572 | 573 | SEE ALSO 574 | ======== 575 | B{delete} 576 | ''' 577 | self.assert_root() 578 | try: 579 | tpg_lun = int(tpg_lun) 580 | mapped_lun = int(mapped_lun) 581 | except ValueError: 582 | self.shell.log.error("Incorrect LUN value.") 583 | return 584 | 585 | if tpg_lun in (ml.tpg_lun.lun for ml in self.rtsnode.mapped_luns): 586 | self.shell.log.warning( 587 | "Warning: TPG LUN %d already mapped to this NodeACL" % tpg_lun) 588 | 589 | mlun = MappedLUN(self.rtsnode, mapped_lun, tpg_lun, write_protect) 590 | ui_mlun = UIMappedLUN(mlun, self) 591 | self.shell.log.info("Created Mapped LUN %s." % mlun.mapped_lun) 592 | return self.new_node(ui_mlun) 593 | 594 | def ui_command_delete(self, mapped_lun): 595 | ''' 596 | Deletes the specified I{mapped_lun}. 597 | 598 | SEE ALSO 599 | ======== 600 | B{create} 601 | ''' 602 | self.assert_root() 603 | mlun = MappedLUN(self.rtsnode, mapped_lun) 604 | mlun.delete() 605 | self.shell.log.info("Deleted Mapped LUN %s." % mapped_lun) 606 | self.refresh() 607 | 608 | def ui_complete_delete(self, parameters, text, current_param): 609 | ''' 610 | Parameter auto-completion method for user command delete. 611 | @param parameters: Parameters on the command line. 612 | @type parameters: dict 613 | @param text: Current text of parameter being typed by the user. 614 | @type text: str 615 | @param current_param: Name of parameter to complete. 616 | @type current_param: str 617 | @return: Possible completions 618 | @rtype: list of str 619 | ''' 620 | if current_param == 'mapped_lun': 621 | mluns = [str(mlun.mapped_lun) for mlun in self.rtsnode.mapped_luns] 622 | completions = [mlun for mlun in mluns if mlun.startswith(text)] 623 | else: 624 | completions = [] 625 | 626 | if len(completions) == 1: 627 | return [completions[0] + ' '] 628 | else: 629 | return completions 630 | 631 | 632 | class UIMappedLUN(UIRTSLibNode): 633 | ''' 634 | A generic UI for MappedLUN objects. 635 | ''' 636 | def __init__(self, mapped_lun, parent): 637 | name = "mapped_lun%d" % mapped_lun.mapped_lun 638 | UIRTSLibNode.__init__(self, name, mapped_lun, parent) 639 | self.cfs_cwd = mapped_lun.path 640 | self.refresh() 641 | 642 | def summary(self): 643 | mapped_lun = self.rtsnode 644 | is_healthy = True 645 | try: 646 | tpg_lun = mapped_lun.tpg_lun 647 | except RTSLibBrokenLink: 648 | description = "BROKEN LUN LINK" 649 | is_healthy = False 650 | else: 651 | if mapped_lun.write_protect: 652 | access_mode = 'ro' 653 | else: 654 | access_mode = 'rw' 655 | description = "lun%d (%s)" % (tpg_lun.lun, access_mode) 656 | 657 | return (description, is_healthy) 658 | 659 | 660 | class UILUNs(UINode): 661 | ''' 662 | A generic UI for TPG LUNs. 663 | ''' 664 | def __init__(self, tpg, parent): 665 | UINode.__init__(self, "luns", parent) 666 | self.cfs_cwd = "%s/lun" % tpg.path 667 | self.tpg = tpg 668 | self.refresh() 669 | 670 | def refresh(self): 671 | self._children = set([]) 672 | for lun in self.tpg.luns: 673 | UILUN(lun, self) 674 | 675 | def summary(self): 676 | no_luns = len(self._children) 677 | if no_luns != 1: 678 | msg = "%d LUNs" % no_luns 679 | else: 680 | msg = "%d LUN" % no_luns 681 | return (msg, None) 682 | 683 | def ui_command_create(self, storage_object, lun=None, 684 | add_mapped_luns=None): 685 | ''' 686 | Creates a new LUN in the Target Portal Group, attached to a storage 687 | object. If the I{lun} parameter is omitted, the first available LUN in 688 | the TPG will be used. If present, it must be a number greater than 0. 689 | Alternatively, the syntax I{lunX} where I{X} is a positive number is 690 | also accepted. 691 | 692 | The I{storage_object} must be the path of an existing storage object, 693 | i.e. B{/backstore/pscsi0/mydisk} to reference the B{mydisk} storage 694 | object of the virtual HBA B{pscsi0}. 695 | 696 | If I{add_mapped_luns} is omitted, the global parameter 697 | B{auto_add_mapped_luns} will be used, else B{true} or B{false} are 698 | accepted. If B{true}, then after creating the LUN, mapped LUNs will be 699 | automatically created for all existing node ACLs, mapping the new LUN. 700 | 701 | SEE ALSO 702 | ======== 703 | B{delete} 704 | ''' 705 | self.assert_root() 706 | if lun is None: 707 | luns = [lun.lun for lun in self.tpg.luns] 708 | for index in range(1048576): 709 | if index not in luns: 710 | lun = index 711 | break 712 | if lun is None: 713 | self.shell.log.error("Cannot find an available LUN.") 714 | return 715 | else: 716 | self.shell.log.info("Selected LUN %d." % lun) 717 | else: 718 | try: 719 | if lun.startswith('lun'): 720 | lun = lun[3:] 721 | lun = int(lun) 722 | except ValueError: 723 | self.shell.log.error("The LUN must be an integer value.") 724 | return 725 | else: 726 | if lun < 0: 727 | self.shell.log.error("The LUN cannot be negative.") 728 | return 729 | 730 | add_mapped_luns = \ 731 | self.ui_eval_param(add_mapped_luns, 'bool', 732 | self.shell.prefs['auto_add_mapped_luns']) 733 | 734 | try: 735 | storage_object = self.get_node(storage_object).rtsnode 736 | except ValueError: 737 | self.shell.log.error("Invalid storage object %s." % storage_object) 738 | return 739 | 740 | lun_object = LUN(self.tpg, lun, storage_object) 741 | self.shell.log.info("Created LUN %s." % lun_object.lun) 742 | ui_lun = UILUN(lun_object, self) 743 | 744 | if add_mapped_luns: 745 | for acl in self.tpg.node_acls: 746 | mapped_lun = lun 747 | existing_mluns = [mlun.mapped_lun for mlun in acl.mapped_luns] 748 | if mapped_lun in existing_mluns: 749 | tentative_mlun = 0 750 | while mapped_lun == lun: 751 | if tentative_mlun not in existing_mluns: 752 | mapped_lun = tentative_mlun 753 | self.shell.log.warning( 754 | "Mapped LUN %d already " % lun 755 | + "exists in ACL %s, using %d instead." 756 | % (acl.node_wwn, mapped_lun)) 757 | else: 758 | tentative_mlun += 1 759 | mlun = MappedLUN(acl, mapped_lun, lun, write_protect=False) 760 | self.shell.log.info("Created mapped LUN %d in node ACL %s" 761 | % (mapped_lun, acl.node_wwn)) 762 | self.parent.refresh() 763 | 764 | return self.new_node(ui_lun) 765 | 766 | def ui_complete_create(self, parameters, text, current_param): 767 | ''' 768 | Parameter auto-completion method for user command create. 769 | @param parameters: Parameters on the command line. 770 | @type parameters: dict 771 | @param text: Current text of parameter being typed by the user. 772 | @type text: str 773 | @param current_param: Name of parameter to complete. 774 | @type current_param: str 775 | @return: Possible completions 776 | @rtype: list of str 777 | ''' 778 | if current_param == 'storage_object': 779 | storage_objects = [] 780 | for backstore in self.get_node('/backstores').children: 781 | for storage_object in backstore.children: 782 | storage_objects.append(storage_object.path) 783 | completions = [so for so in storage_objects if so.startswith(text)] 784 | else: 785 | completions = [] 786 | 787 | if len(completions) == 1: 788 | return [completions[0] + ' '] 789 | else: 790 | return completions 791 | 792 | def ui_command_delete(self, lun): 793 | ''' 794 | Deletes the supplied LUN from the Target Portal Group. The I{lun} must 795 | be a positive number matching an existing LUN. 796 | 797 | Alternatively, the syntax I{lunX} where I{X} is a positive number is 798 | also accepted. 799 | 800 | SEE ALSO 801 | ======== 802 | B{create} 803 | ''' 804 | self.assert_root() 805 | if lun.lower().startswith("lun"): 806 | lun = lun[3:] 807 | try: 808 | lun = int(lun) 809 | lun_object = LUN(self.tpg, lun) 810 | except: 811 | raise RTSLibError("Invalid LUN") 812 | lun_object.delete() 813 | self.shell.log.info("Deleted LUN %s." % lun) 814 | # Refresh the TPG as we need to also refresh acls MappedLUNs 815 | self.parent.refresh() 816 | 817 | def ui_complete_delete(self, parameters, text, current_param): 818 | ''' 819 | Parameter auto-completion method for user command delete. 820 | @param parameters: Parameters on the command line. 821 | @type parameters: dict 822 | @param text: Current text of parameter being typed by the user. 823 | @type text: str 824 | @param current_param: Name of parameter to complete. 825 | @type current_param: str 826 | @return: Possible completions 827 | @rtype: list of str 828 | ''' 829 | if current_param == 'lun': 830 | luns = [str(lun.lun) for lun in self.tpg.luns] 831 | completions = [lun for lun in luns if lun.startswith(text)] 832 | else: 833 | completions = [] 834 | 835 | if len(completions) == 1: 836 | return [completions[0] + ' '] 837 | else: 838 | return completions 839 | 840 | 841 | class UILUN(UIRTSLibNode): 842 | ''' 843 | A generic UI for LUN objects. 844 | ''' 845 | def __init__(self, lun, parent): 846 | name = "lun%d" % lun.lun 847 | UIRTSLibNode.__init__(self, name, lun, parent) 848 | self.cfs_cwd = lun.path 849 | self.refresh() 850 | 851 | def summary(self): 852 | lun = self.rtsnode 853 | is_healthy = True 854 | try: 855 | storage_object = lun.storage_object 856 | except RTSLibBrokenLink: 857 | description = "BROKEN STORAGE LINK" 858 | is_healthy = False 859 | else: 860 | backstore = storage_object.backstore 861 | if backstore.plugin.startswith("rd"): 862 | path = "ramdisk" 863 | else: 864 | path = storage_object.udev_path 865 | if self.shell.prefs['legacy_hba_view']: 866 | description = "%s%s/%s (%s)" % (backstore.plugin, 867 | backstore.index, 868 | storage_object.name, path) 869 | else: 870 | description = "%s/%s (%s)" % (backstore.plugin, 871 | dedup_so_name(storage_object), 872 | path) 873 | 874 | return (description, is_healthy) 875 | 876 | 877 | class UIPortals(UINode): 878 | ''' 879 | A generic UI for TPG network portals. 880 | ''' 881 | def __init__(self, tpg, parent): 882 | UINode.__init__(self, "portals", parent) 883 | self.tpg = tpg 884 | self.cfs_cwd = "%s/np" % tpg.path 885 | self.refresh() 886 | 887 | def refresh(self): 888 | self._children = set([]) 889 | for portal in self.tpg.network_portals: 890 | UIPortal(portal, self) 891 | 892 | def summary(self): 893 | no_portals = len(self._children) 894 | if no_portals != 1: 895 | msg = "%d Portals" % no_portals 896 | else: 897 | msg = "%d Portal" % no_portals 898 | return (msg, None) 899 | 900 | def ui_command_create(self, ip_address=None, ip_port=None): 901 | ''' 902 | Creates a Network Portal with specified I{ip_address} and I{ip_port}. 903 | If I{ip_port} is omitted, the default port for the target fabric will 904 | be used. If I{ip_address} is omitted, the first IP address found 905 | matching the local hostname will be used. 906 | 907 | SEE ALSO 908 | ======== 909 | B{delete} 910 | ''' 911 | self.assert_root() 912 | try: 913 | listen_all = int(ip_address.replace(".", "")) == 0 914 | except: 915 | listen_all = False 916 | if listen_all: 917 | ip_address = "0.0.0.0" 918 | if ip_port is None: 919 | # FIXME: Add a specfile parameter to determine that 920 | ip_port = 3260 921 | self.shell.log.info("Using default IP port %d" % ip_port) 922 | if ip_address is None: 923 | if not ip_address: 924 | ip_address = utils.get_main_ip() 925 | if ip_address: 926 | self.shell.log.info("Automatically selected IP address %s." 927 | % ip_address) 928 | else: 929 | self.shell.log.error("Cannot find a usable IP address to " 930 | + "create the Network Portal.") 931 | return 932 | elif ip_address not in utils.list_eth_ips() and not listen_all: 933 | self.shell.log.error("IP address does not exist: %s" % ip_address) 934 | return 935 | 936 | try: 937 | ip_port = int(ip_port) 938 | except ValueError: 939 | self.shell.log.error("The ip_port must be an integer value.") 940 | return 941 | 942 | portal = NetworkPortal(self.tpg, ip_address, ip_port, mode='create') 943 | self.shell.log.info("Created network portal %s:%d." 944 | % (ip_address, ip_port)) 945 | ui_portal = UIPortal(portal, self) 946 | return self.new_node(ui_portal) 947 | 948 | def ui_complete_create(self, parameters, text, current_param): 949 | ''' 950 | Parameter auto-completion method for user command create. 951 | @param parameters: Parameters on the command line. 952 | @type parameters: dict 953 | @param text: Current text of parameter being typed by the user. 954 | @type text: str 955 | @param current_param: Name of parameter to complete. 956 | @type current_param: str 957 | @return: Possible completions 958 | @rtype: list of str 959 | ''' 960 | if current_param == 'ip_address': 961 | completions = [addr for addr in utils.list_eth_ips() 962 | if addr.startswith(text)] 963 | else: 964 | completions = [] 965 | 966 | if len(completions) == 1: 967 | return [completions[0] + ' '] 968 | else: 969 | return completions 970 | 971 | def ui_command_delete(self, ip_address, ip_port): 972 | ''' 973 | Deletes the Network Portal with specified I{ip_address} and I{ip_port}. 974 | 975 | SEE ALSO 976 | ======== 977 | B{create} 978 | ''' 979 | self.assert_root() 980 | portal = NetworkPortal(self.tpg, ip_address, ip_port, mode='lookup') 981 | portal.delete() 982 | self.shell.log.info("Deleted network portal %s:%s" 983 | % (ip_address, ip_port)) 984 | self.refresh() 985 | 986 | def ui_complete_delete(self, parameters, text, current_param): 987 | ''' 988 | Parameter auto-completion method for user command delete. 989 | @param parameters: Parameters on the command line. 990 | @type parameters: dict 991 | @param text: Current text of parameter being typed by the user. 992 | @type text: str 993 | @param current_param: Name of parameter to complete. 994 | @type current_param: str 995 | @return: Possible completions 996 | @rtype: list of str 997 | ''' 998 | completions = [] 999 | # TODO: Check if a dict comprehension is acceptable here with supported 1000 | # XXX: python versions. 1001 | portals = {} 1002 | all_ports = set([]) 1003 | for portal in self.tpg.network_portals: 1004 | all_ports.add(str(portal.port)) 1005 | if not portal.ip_address in portals: 1006 | portals[portal.ip_address] = [] 1007 | portals[portal.ip_address].append(str(portal.port)) 1008 | 1009 | if current_param == 'ip_address': 1010 | if 'ip_port' in parameters: 1011 | port = parameters['ip_port'] 1012 | completions = [addr for addr in portals 1013 | if port in portals[addr] 1014 | if addr.startswith(text)] 1015 | else: 1016 | completions = [addr for addr in portals 1017 | if addr.startswith(text)] 1018 | elif current_param == 'ip_port': 1019 | if 'ip_address' in parameters: 1020 | addr = parameters['ip_address'] 1021 | if addr in portals: 1022 | completions = [port for port in portals[addr] 1023 | if port.startswith(text)] 1024 | else: 1025 | completions = [port for port in all_ports 1026 | if port.startswith(text)] 1027 | 1028 | if len(completions) == 1: 1029 | return [completions[0] + ' '] 1030 | else: 1031 | return completions 1032 | 1033 | 1034 | class UIPortal(UIRTSLibNode): 1035 | ''' 1036 | A generic UI for a network portal. 1037 | ''' 1038 | def __init__(self, portal, parent): 1039 | name = "%s:%s" % (portal.ip_address, portal.port) 1040 | UIRTSLibNode.__init__(self, name, portal, parent) 1041 | self.cfs_cwd = portal.path 1042 | self.portal = portal 1043 | self.refresh() 1044 | 1045 | def summary(self): 1046 | if self.portal._get_iser_attr(): 1047 | return ('OK, iser enabled', True) 1048 | else: 1049 | return ('OK, iser disabled', True) 1050 | 1051 | def ui_command_iser_enable(self): 1052 | ''' 1053 | Enables iser operation on an network portal. 1054 | ''' 1055 | if self.portal._get_iser_attr() == True: 1056 | self.shell.log.info("iser operation has already been enabled") 1057 | else: 1058 | self.portal._set_iser_attr(True) 1059 | self.shell.log.info("iser operation has been enabled") 1060 | 1061 | def ui_command_iser_disable(self): 1062 | ''' 1063 | Disabled iser operation on an network portal. 1064 | ''' 1065 | if self.portal._get_iser_attr() == False: 1066 | self.shell.log.info("iser operation has already been disabled") 1067 | else: 1068 | self.portal._set_iser_attr(False) 1069 | self.shell.log.info("iser operation has been disabled") 1070 | --------------------------------------------------------------------------------