pax_global_header00006660000000000000000000000064127015772440014523gustar00rootroot0000000000000052 comment=b72527b10d5317d67141986b03500a323ea01cd2 targetcli-fb-2.1.fb43/000077500000000000000000000000001270157724400144655ustar00rootroot00000000000000targetcli-fb-2.1.fb43/.gitignore000066400000000000000000000005731270157724400164620ustar00rootroot00000000000000debian/changelog dpkg-buildpackage.log dpkg-buildpackage.version build-stamp build/ debian/files debian/rtsadmin-doc.debhelper.log debian/rtsadmin-doc.substvars debian/rtsadmin-doc/ debian/rtsadmin.debhelper.log debian/rtsadmin.substvars debian/rtsadmin/ debian/tmp/ dist/ doc/ *.pyc *.swp dpkg-buildpackage.log dpkg-buildpackage.version ./*.spec redhat/*.spec ./rtsadmin-* log/ targetcli-fb-2.1.fb43/COPYING000066400000000000000000000236371270157724400155330ustar00rootroot00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. targetcli-fb-2.1.fb43/Makefile000066400000000000000000000107471270157724400161360ustar00rootroot00000000000000PKGNAME = targetcli-fb NAME = targetcli GIT_BRANCH = $$(git branch | grep \* | tr -d \*) VERSION = $$(basename $$(git describe --tags | tr - . | sed 's/^v//')) all: @echo "Usage:" @echo @echo " make deb - Builds debian packages." @echo " make rpm - Builds rpm packages." @echo " make release - Generates the release tarball." @echo @echo " make clean - Cleanup the local repository build files." @echo " make cleanall - Also remove dist/*" clean: @rm -fv ${NAME}/*.pyc ${NAME}/*.html @rm -frv doc @rm -frv ${NAME}.egg-info MANIFEST build @rm -frv debian/tmp @rm -fv build-stamp @rm -fv dpkg-buildpackage.log dpkg-buildpackage.version @rm -frv *.rpm @rm -fv debian/files debian/*.log debian/*.substvars @rm -frv debian/${PKGNAME}-doc/ debian/python2.5-${PKGNAME}/ @rm -frv debian/python2.6-${PKGNAME}/ debian/python-${PKGNAME}/ @rm -frv results @rm -fv rpm/*.spec *.spec rpm/sed* sed* @rm -frv ${NAME}-* @rm -frv *.rpm warn${NAME}.txt build${NAME} @rm -fv debian/*.debhelper.log debian/*.debhelper debian/*.substvars debian/files @rm -fvr debian/${NAME}-frozen/ debian/${NAME}-python2.5/ @rm -fvr debian/${NAME}-python2.6/ debian/${NAME}/ debian/${NAME}-doc/ @rm -frv log/ @echo "Finished cleanup." cleanall: clean @rm -frv dist release: build/release-stamp build/release-stamp: @mkdir -p build @echo "Exporting the repository files..." @git archive ${GIT_BRANCH} --prefix ${PKGNAME}-${VERSION}/ \ | (cd build; tar xfp -) @echo "Cleaning up the target tree..." @rm -f build/${PKGNAME}-${VERSION}/Makefile @rm -f build/${PKGNAME}-${VERSION}/.gitignore @rm -rf build/${PKGNAME}-${VERSION}/bin @echo "Fixing version string..." @sed -i "s/__version__ = .*/__version__ = '${VERSION}'/g" \ build/${PKGNAME}-${VERSION}/${NAME}/__init__.py @echo "Generating rpm specfile from template..." @cd build/${PKGNAME}-${VERSION}; \ for spectmpl in rpm/*.spec.tmpl; do \ sed -i "s/Version:\( *\).*/Version:\1${VERSION}/g" $${spectmpl}; \ mv $${spectmpl} $$(basename $${spectmpl} .tmpl); \ done; \ rmdir rpm @echo "Generating rpm changelog..." @( \ version=${VERSION}; \ author=$$(git show HEAD --format="format:%an <%ae>" -s); \ date=$$(git show HEAD --format="format:%ad" -s \ | awk '{print $$1,$$2,$$3,$$5}'); \ hash=$$(git show HEAD --format="format:%H" -s); \ echo '* '"$${date} $${author} $${version}-1"; \ echo " - Generated from git commit $${hash}."; \ ) >> $$(ls build/${PKGNAME}-${VERSION}/*.spec) @echo "Generating debian changelog..." @( \ version=${VERSION}; \ author=$$(git show HEAD --format="format:%an <%ae>" -s); \ date=$$(git show HEAD --format="format:%aD" -s); \ day=$$(git show HEAD --format='format:%ai' -s \ | awk '{print $$1}' \ | awk -F '-' '{print $$3}' | sed 's/^0/ /g'); \ date=$$(echo $${date} \ | awk '{print $$1, "'"$${day}"'", $$3, $$4, $$5, $$6}'); \ hash=$$(git show HEAD --format="format:%H" -s); \ echo "${PKGNAME} ($${version}) unstable; urgency=low"; \ echo; \ echo " * Generated from git commit $${hash}."; \ echo; \ echo " -- $${author} $${date}"; \ echo; \ ) > build/${PKGNAME}-${VERSION}/debian/changelog @find build/${PKGNAME}-${VERSION}/ -exec \ touch -t $$(date -d @$$(git show -s --format="format:%at") \ +"%Y%m%d%H%M.%S") {} \; @mkdir -p dist @cd build; tar -c --owner=0 --group=0 --numeric-owner \ --format=gnu -b20 --quoting-style=escape \ -f ../dist/${PKGNAME}-${VERSION}.tar \ $$(find ${PKGNAME}-${VERSION} -type f | sort) @gzip -6 -n dist/${PKGNAME}-${VERSION}.tar @echo "Generated release tarball:" @echo " $$(ls dist/${PKGNAME}-${VERSION}.tar.gz)" @touch build/release-stamp deb: release build/deb-stamp build/deb-stamp: @echo "Building debian packages..." @cd build/${PKGNAME}-${VERSION}; \ dpkg-buildpackage -rfakeroot -us -uc @mv build/*_${VERSION}_*.deb dist/ @echo "Generated debian packages:" @for pkg in $$(ls dist/*_${VERSION}_*.deb); do echo " $${pkg}"; done @touch build/deb-stamp rpm: release build/rpm-stamp build/rpm-stamp: @echo "Building rpm packages..." @mkdir -p build/rpm @build=$$(pwd)/build/rpm; dist=$$(pwd)/dist/; rpmbuild \ --define "_topdir $${build}" --define "_sourcedir $${dist}" \ --define "_rpmdir $${build}" --define "_buildir $${build}" \ --define "_srcrpmdir $${build}" -ba build/${PKGNAME}-${VERSION}/*.spec @mv build/rpm/*-${VERSION}*.src.rpm dist/ @mv build/rpm/*/*-${VERSION}*.rpm dist/ @echo "Generated rpm packages:" @for pkg in $$(ls dist/*-${VERSION}*.rpm); do echo " $${pkg}"; done @touch build/rpm-stamp targetcli-fb-2.1.fb43/README.md000066400000000000000000000033561270157724400157530ustar00rootroot00000000000000targetcli-fb ============ A command shell for managing the Linux LIO kernel target -------------------------------------------------------- targetcli-fb is a command-line interface for configuring the LIO generic SCSI target, present in 3.x Linux kernel versions. Compatible with both Python 2.7 and 3.x by using the python-six library. targetcli-fb development ------------------------ targetcli-fb is licensed under the Apache 2.0 license. Contributions are welcome. * Mailing list: [targetcli-fb-devel](https://lists.fedorahosted.org/mailman/listinfo/targetcli-fb-devel) * Source repo: [GitHub](https://github.com/agrover/targetcli-fb) * Bugs: [GitHub](https://github.com/agrover/targetcli-fb/issues) or [Trac](https://fedorahosted.org/targetcli-fb/) * Tarballs: [fedorahosted](https://fedorahosted.org/releases/t/a/targetcli-fb/) * Playlist of instructional screencast videos: [YouTube](https://www.youtube.com/playlist?list=PLC2C75481A3ABB067) In-repo packaging ----------------- Packaging scripts for RPM and DEB are included, but these are to make end-user custom packaging easier -- distributions tend to maintain their own packaging scripts separately. If you run into issues with packaging, start with opening a bug on your distro's bug reporting system. Some people do use these scripts, so we want to keep them around. Fixes for any breakage you encounter are welcome. "fb" -- "free branch" --------------------- targetcli-fb is a fork of the "targetcli" code written by RisingTide Systems. The "-fb" differentiates between the original and this version. Please ensure to use either all "fb" versions of the targetcli components -- targetcli, rtslib, and configshell, or stick with all non-fb versions, since they are no longer strictly compatible. targetcli-fb-2.1.fb43/THANKS000066400000000000000000000010361270157724400154000ustar00rootroot00000000000000contribution: Johannes Dewender contribution: Jerome Martin contribution: Nicholas Bellinger contribution: Rafiu Fakunle contribution: António Meireles contribution: Andy Grover contribution: Tregaron Bayly contribution: Christophe Vu-Brugier testing: Dax Kelson testing: Gris Ge targetcli-fb-2.1.fb43/debian/000077500000000000000000000000001270157724400157075ustar00rootroot00000000000000targetcli-fb-2.1.fb43/debian/README.Debian000066400000000000000000000010631270157724400177500ustar00rootroot00000000000000Copyright (c) 2011-2013 by Datera, Inc Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. targetcli-fb-2.1.fb43/debian/compat000066400000000000000000000000021270157724400171050ustar00rootroot000000000000005 targetcli-fb-2.1.fb43/debian/control000066400000000000000000000007761270157724400173240ustar00rootroot00000000000000Source: targetcli-fb Section: python Priority: optional Maintainer: Andy Grover Build-Depends: debhelper(>= 8), python3 , python3-setuptools Standards-Version: 3.9.4 X-Python3-Version: >= 3.1 Package: targetcli-fb Architecture: all Depends: python3 (>= 3.1), ${misc:Depends}, python3-configshell-fb, python3-rtslib-fb Conflicts: targetcli, targetcli-frozen, lio-utils Description: CLI shell for the RisingTide Systems target (free branch). . This package contains the targetcli CLI. targetcli-fb-2.1.fb43/debian/copyright000066400000000000000000000015141270157724400176430ustar00rootroot00000000000000This package was originally debianized by Jerome Martin on Thu Nov 19 12:00:01 UTC 2009. It is currently maintained by Andy Grover . Upstream Author: Jerome Martin This file is part of ConfigShell. Copyright (c) 2011-2013 by Datera, Inc Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. targetcli-fb-2.1.fb43/debian/manpages000066400000000000000000000000141270157724400174200ustar00rootroot00000000000000targetcli.8 targetcli-fb-2.1.fb43/debian/pyversions000066400000000000000000000000051270157724400200460ustar00rootroot000000000000002.6- targetcli-fb-2.1.fb43/debian/rules000077500000000000000000000007461270157724400167760ustar00rootroot00000000000000#!/usr/bin/make -f build_dir = build install_dir = $(CURDIR)/debian/targetcli-fb #export DH_VERBOSE=1 PYTHON3=$(shell py3versions -vr) # prevent internet access / don't use PyPi export http_proxy = http://127.0.0.1:9 %: dh $@ --with python3 override_dh_auto_build: python$(PYTHON3) setup.py build override_dh_auto_install: python$(PYTHON3) setup.py install --root=$(install_dir) --install-layout=deb override_dh_auto_clean: dh_auto_clean rm -rf build rm -rf *.egg-info targetcli-fb-2.1.fb43/rpm/000077500000000000000000000000001270157724400152635ustar00rootroot00000000000000targetcli-fb-2.1.fb43/rpm/targetcli.spec.tmpl000066400000000000000000000016641270157724400210770ustar00rootroot00000000000000%define oname targetcli-fb Name: targetcli License: Apache License 2.0 Group: Applications/System Summary: RisingTide Systems generic SCSI target CLI shell. Version: VERSION Release: 1%{?dist} URL: http://www.risingtidesystems.com/git/ Source: %{oname}-%{version}.tar.gz BuildRoot: %{_tmppath}/%{name}-%{version}-%{release}-rpmroot BuildArch: noarch BuildRequires: python-devel, python-rtslib, python-configshell Requires: python-rtslib, python-configshell Vendor: Datera, Inc. %description RisingTide Systems generic SCSI target CLI shell. %prep %setup -q -n %{oname}-%{version} %build %{__python} setup.py build %install rm -rf %{buildroot} %{__python} setup.py install --skip-build --root=%{buildroot} --prefix=usr %clean rm -rf %{buildroot} %files %defattr(-,root,root,-) %{python_sitelib} %{_bindir}/targetcli %doc COPYING README.md %changelog targetcli-fb-2.1.fb43/scripts/000077500000000000000000000000001270157724400161545ustar00rootroot00000000000000targetcli-fb-2.1.fb43/scripts/targetcli000077500000000000000000000075771270157724400201000ustar00rootroot00000000000000#!/usr/bin/python ''' Starts the targetcli CLI shell. This file is part of targetcli. Copyright (c) 2011-2013 by Datera, Inc Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ''' from __future__ import print_function from os import getuid from targetcli import UIRoot from rtslib_fb import RTSLibError from configshell_fb import ConfigShell, ExecutionError import sys from targetcli import __version__ as targetcli_version err = sys.stderr class TargetCLI(ConfigShell): default_prefs = {'color_path': 'magenta', 'color_command': 'cyan', 'color_parameter': 'magenta', 'color_keyword': 'cyan', 'completions_in_columns': True, 'logfile': None, 'loglevel_console': 'info', 'loglevel_file': 'debug9', 'color_mode': True, 'prompt_length': 30, 'tree_max_depth': 0, 'tree_status_mode': True, 'tree_round_nodes': True, 'tree_show_root': True, 'export_backstore_name_as_model': True, 'auto_enable_tpgt': True, 'auto_add_mapped_luns': True, 'auto_cd_after_create': False, 'auto_save_on_exit': True, 'auto_add_default_portal': True, } def usage(): print("Usage: %s [--version|--help|CMD]" % sys.argv[0], file=err) print(" --version\t\tPrint version", file=err) print(" --help\t\tPrint this information", file=err) print(" CMD\t\t\tRun targetcli shell command and exit", file=err) print(" \t\tEnter configuration shell", file=err) print("See man page for more information.", file=err) sys.exit(-1) def version(): print("%s version %s" % (sys.argv[0], targetcli_version), file=err) sys.exit(-1) def main(): ''' Start the targetcli shell. ''' if getuid() == 0: is_root = True else: is_root = False shell = TargetCLI('~/.targetcli') try: root_node = UIRoot(shell, as_root=is_root) root_node.refresh() except Exception as error: shell.con.display(shell.con.render_text(str(error), 'red')) if not is_root: shell.con.display(shell.con.render_text("Retry as root.", 'red')) sys.exit(-1) if len(sys.argv) > 1: if sys.argv[1] in ("--help", "-h"): usage() if sys.argv[1] in ("--version", "-v"): version() try: shell.run_cmdline(" ".join(sys.argv[1:])) except Exception as e: print(str(e), file=sys.stderr) sys.exit(1) sys.exit(0) shell.con.display("targetcli shell version %s\n" "Copyright 2011-2013 by Datera, Inc and others.\n" "For help on commands, type 'help'.\n" % targetcli_version) if not is_root: shell.con.display("You are not root, disabling privileged commands.\n") while not shell._exit: try: shell.run_interactive() except (RTSLibError, ExecutionError) as msg: shell.log.error(str(msg)) if shell.prefs['auto_save_on_exit'] and is_root: shell.log.info("Global pref auto_save_on_exit=true") root_node.ui_command_saveconfig() if __name__ == "__main__": main() targetcli-fb-2.1.fb43/setup.py000077500000000000000000000023511270157724400162030ustar00rootroot00000000000000#! /usr/bin/env python ''' This file is part of targetcli. Copyright (c) 2011-2013 by Datera, Inc Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ''' from setuptools import setup __version__ = '' exec(open('targetcli/version.py').read()) setup( name = 'targetcli-fb', version = __version__, description = 'An administration shell for RTS storage targets.', license = 'Apache 2.0', maintainer = 'Andy Grover', maintainer_email = 'agrover@redhat.com', url = 'http://github.com/agrover/targetcli-fb', packages = ['targetcli'], scripts = ['scripts/targetcli'], classifiers = [ "Programming Language :: Python", "Programming Language :: Python :: 3", "License :: OSI Approved :: Apache Software License", ], ) targetcli-fb-2.1.fb43/source/000077500000000000000000000000001270157724400157655ustar00rootroot00000000000000targetcli-fb-2.1.fb43/source/format000066400000000000000000000000151270157724400171740ustar00rootroot000000000000003.0 (native) targetcli-fb-2.1.fb43/targetcli.8000066400000000000000000000367401270157724400165460ustar00rootroot00000000000000.TH targetcli 8 .SH NAME .B targetcli \- administration shell for storage targets .SH DESCRIPTION .B targetcli is a shell for viewing, editing, and saving the configuration of the kernel's target subsystem, also known as LIO. It enables the administrator to assign local storage resources backed by either files, volumes, local SCSI devices, or ramdisk, and export them to remote systems via network fabrics, such as iSCSI or FCoE. .P The configuration layout is tree-based, similar to a filesystem, and is navigated in a similar manner. .SH USAGE .B targetcli .P .B targetcli [cmd] .P Invoke .B targetcli as root to enter the configuration shell, or follow with a command to execute but do not enter the shell. Use .B ls to list nodes below the current path. Moving around the tree is accomplished by the .B cd command, or by entering the new location directly. Objects are created using .BR create , removed using .BR delete . Use .B "help " for additional usage information. Tab-completion is available for commands and command arguments. .P Configuration changes in targetcli are made immediately to the underlying kernel target configuration. Settings will not be retained across reboot unless .B saveconfig is either explicitly called, or implicitly by exiting the shell with the global preference .B auto_save_on_exit set to .BR true , the default. .P .SH QUICKSTART To create an iSCSI target and share a file-backed LUN without any auth checks: .P $ sudo targetcli .br /> backstores/fileio create test /tmp/test.img 100m .br /> iscsi/ create iqn.2006-04.example.com:test-target .br /> cd iscsi/iqn.2006-04.example.com:test-target/tpg1/ .br tpg1/> luns/ create /backstores/fileio/test .br tpg1/> set attribute generate_node_acls=1 .br tpg1/> exit .P Although by default targetcli saves the running configuration upon exit, a distribution-specific service must be enabled to restore the saved configuration on reboot. See distribution documentation for specifics, but for example: .P $ sudo systemctl enable target.service .P See .B EXAMPLES below for more detailed information on commands and using the shell. .SH BACKSTORES .B Backstores are different kinds of local storage resources that the kernel target uses to "back" the SCSI devices it exports. The mappings to local storage resources that each backstore creates are called .BR "storage objects" . .SS FILEIO Allows files to be treated as disk images. When storage objects of this type are created, they can support either write-back or write-thru operation. Using write-back enables the local filesystem cache, which will improve performance but increase the risk of data loss. It is also possible to use fileio with local block device files, if buffered operation is needed. .P Fileio also supports using an existing file, or creating a new file. New files are sparsely allocated by default. .SS BLOCK Allows a local disk block device to be shared. .SS PSCSI Allows a local SCSI device of any type to be shared. It is generally advised to prefer the block backstore if sharing a block SCSI device is desired. .SS RAMDISK Allows kernel memory to be shared as a block SCSI device. Since memory is volatile, the contents of the ramdisk will be lost if the system restarts, and this backstore is best used for testing only. .P It also supports "nullio" mode, which is not backed by any storage. It discards all writes, and returns all-zeroes for reads. .SS USERSPACE-BACKED Backstores starting with "user:" are not supported in the kernel, but rely on a userspace process to handle requests. See .BR tcmu-runner (8) for more information on creating backstores of this type. .SH TARGETS .B Targets are instances of a .BR fabric , which adapts the kernel target to a specific transport protocol such as iSCSI, Fibre Channel, or SBP-2. Creating a target in targetcli enables that target to be configured. The name of the target, its WWN (world wide name), may link the configuration to a specific hardware endpoint, like SRP for example, or it may not, like iSCSI. .P Aside from "backstores", all other top-level configuration nodes in targetcli are fabrics that may have targets created for them. Fabrics that require hardware are only listed if the hardware is present and configured properly. .SS CREATING A TARGET Use the .B create command within a fabric's node to create a target. If the fabric's targets are tied to hardware then targetcli will constrain the WWN to available hardware WWNs. These can be shown via tab-completion. If the fabric is not tied to hardware, such as iSCSI, then targetcli will either auto-generate a WWN if none is given, or check that the given WWN has the correct format. All WWNs are prefaced by their type, such as "iqn", "naa", or "ib", and may be given with or without colons. .P iSCSI supports multiple WWN formats: iqn, naa, and eui. Other fabrics support single formats only. .SH CONFIGURING A TARGET Not all fabrics have the same capabilities. Targets on different fabrics will have different configuration node layouts. iSCSI has the most to configure; other fabrics present subsets of iSCSI's feature set. .SH CONFIGURING ISCSI iSCSI has the most options for configuration. .SS TPGS TPGs (Target Portal Groups) allow the iSCSI to support multiple complete configurations within one target. This is useful for complex quality-of-service configurations. targetcli will automatically create one TPG when the target is created, and almost all setups only need one. .SS PORTALS An iSCSI target may be reached via multiple IP addresses and ports. These addr:port pairs are called .BR portals . Both IPv4 and IPv6 addresses are supported. .P When a target is created, targetcli automatically creates a default portal listening on all IPv4 addresses (shown as 0.0.0.0) on port 3260. If a different configuration is needed, the default portal can be removed and portals configured as desired. .P If the hardware supports it, .B iSER (iSCSI Extensions for RDMA) may be enabled via the .B enable_iser command within each portal's node. .SS LUNS The kernel target exports SCSI Logical Units, also called .BR LUNs . This section links the previously-defined storage objects with the target, and defines which number (the Logical Unit Number) the device will use. Note that if ACLs are being used, a .B "lun mapping" must be created under the ACL that refers back to the TPG LUN. .SS ACLS ACLs (Access Control Lists) allow different configuration, depending on the initiator that is connecting to the target. This includes both per-initiator authentication settings as well as per-initiator LUN mappings. .P .B "create " in the .B acls node creates an ACL for an initiator, and .B create within the ACL creates a LUN mapping. (This can either refer to the TPG LUN, or to the storage object, in which case the TPG LUN will be configured as well.) Global setting .B auto_add_mapped_luns affects this, see below. .SS AUTHENTICATION iSCSI supports authentication via the CHAP protocol, which uses a username and password. The initiator may be required to supply valid credentials to the target, and the target may also be required to supply credentials back to the initiator. The latter is referred to as .BR "mutual authentication" . .P Furthermore, authentication credentials may be different for each session phase (Discovery or Normal), and authentication in a Normal session may be set at the TPG level, or per-ACL. .P .B Discovery Authentication .br iSCSI Discovery sessions allow the initiator to connect to a portal and discover targets with the SendTargets command, but not access them. The four parameters .BR userid , .BR password , .BR mutual_userid ", and" .B mutual_password are configured via .B "set discovery_auth" command within the top-level iscsi configuration node. 1-way authentication is enabled if userid and password are both set, and mutual authentication is enabled if all four are set. Authentication is disabled by unsetting the parameters. .P .B Normal Authentication .br Similarly, the four parameters .BR userid , .BR password , .BR mutual_userid ", and" .B mutual_password are configured via .B "set auth" command within the TPG node and ACL nodes. However, LIO only uses one or the other, depending on the TPG's .B generate_node_acls attribute setting. If generate_node_acls is 1, the TPG-wide settings will be used. If generate_node_acls is 0, then the user-created ACLs' settings will be used. .P Enable generate_node_acls with .B set attribute generate_node_acls=1 within the TPG node. This can be thought of as "ignore ACLs mode" -- both authentication and LUN mapping will then use the TPG settings. .P .B No Authentication .br Authentication is disabled by clearing the TPG "authentication" attribute: .BR "set attribute authentication=0" . Although initiator names are trivially forgeable, generate_node_acls still works here to either ignore user-defined ACLs and allow all, or check that an ACL exists for the connecting initiator. .SH CONFIGURING FIBRE CHANNEL (QLA2XXX) Operation as a target requires that .B /sys/module/qla2xxx/parameters/qlini_mode report "disabled". This may require passing the .B qlini_mode=disabled parameter to the qla2xxx module when it loads. .SH CONFIGURING FIBRE CHANNEL OVER ETHERNET (TCM_FC) Ensure .B "fcoeadm -i" shows a working endpoint. .SH CONFIGURING SRP SRP (SCSI RDMA Protocol) requires that RDMA-capable hardware is present. It uses "ib" WWNs. .SH CONFIGURING LOOPBACK Storage objects may be re-exported as local SCSI devices with this fabric. .SH CONFIGURING OTHER FABRICS Other fabrics may be present. They are for specialized uses. Use at your own risk. .SH EXAMPLES .SS DEFINING A STORAGE OBJECT WITHIN A BACKSTORE .B backstores/fileio create disk1 /disks/disk1.img 140M .br Creates a storage object named .I disk1 with the given path and size. .B targetcli supports common size abbreviations like 'M', 'G', and 'T'. .P .SS EXPORTING A STORAGE OBJECT VIA ISCSI .B iscsi/ create .br Creates an iSCSI target with a default WWN. It will also create an initial target portal group called .IR tpg1 . .P .B iqn.2003-01.org.linux-iscsi.test2.x8664:sn123456789012/tpg1/ .br An example of changing to the configuration node for the given target's first target portal group (TPG). This is equivalent to giving the command prefixed by "cd". (Although more can be useful for certain setups, most configurations have a single TPG per target. In this case, configuring the TPG is equivalent to configuring the overall target.) .P .B portals/ create .br Add a portal, i.e. an IP address and TCP port via which the target can be contacted by initiators. Only required if the default 0.0.0.0:3260 portal is not present. .P .B luns/ create /backstores/fileio/disk1 .br Create a new LUN in the TPG, attached to the storage object that has previously been defined. The storage object now shows up under the /backstores configuration node as activated. .P .B acls/ create iqn.1994-05.com.redhat:4321576890 .br Creates an ACL (access control list) entry for the given iSCSI initiator. .P .B acls/iqn.1994-05.com.redhat:4321576890 create 2 0 .br Gives the initiator access to the first exported LUN (lun0), which the initiator will see as lun2. The default is to give the initiator read/write access; if read-only access was desired, an additional "1" argument would be added to enable write-protect. (Note: if global setting .B auto_add_mapped_luns is true, this step is not necessary.) .SS EXPORTING A STORAGE OBJECT VIA FCOE .B tcm_fc/ create 20:00:00:19:99:a8:34:bc .br Create an FCoE target with the given WWN. .B targetcli can tab-complete the WWN based on registered FCoE interfaces. If none are found, verify that they are properly configured and are shown in the output of .BR "fcoeadm -i" . .P .B tcm_fc/20:00:00:19:99:a8:34:bc/ .br If .B auto_cd_after_create is set to false, change to the configuration node for the given target, equivalent to giving the command prefixed by .BR cd . .P .B luns/ create /backstores/fileio/disk1 .br Create a new LUN for the interface, attached to a previously defined storage object. The storage object now shows up under the /backstores configuration node as .BR activated . .P .B acls/ create 00:99:88:77:66:55:44:33 .br Create an ACL (access control list), for defining the resources each initiator may access. The default behavior is to auto-map existing LUNs to the ACL; see help for more information. .P The LUN should now be accessible via FCoE. .SH OTHER COMMANDS .B saveconfig .br Save the current configuration settings to a file, from which settings will be restored if the system is rebooted. By default, this will save the configuration to .IR /etc/target/saveconfig.json . .P This command is executed from the configuration root node. .P .B restoreconfig .br Restore target configuration from a file, the default is the file listed under .BR saveconfig . This will fail if there is already an established config, unless the .I clear_existing option is set to .IR true . .P This command is executed from the configuration root node. .P .B clearconfig .br Clears the entire current local configuration. The parameter .I confirm=true must also be given, as a precaution. .P This command is executed from the configuration root node. .P .B sessions [ list | detail ] [sid] .br Lists the current open sessions or a specific session, with or without details. .P This command is executed from the configuration root node. .P .B exit .br Leave the configuration shell. .SH SETTINGS GROUPS Settings are broken into groups. Individual settings are accessed by .B "get " and .BR "set =" , and the settings of an entire group may be displayed by .BR "get " . All except for .I global are associated with a particular configuration node. .SS GLOBAL Shell-related user-specific settings are in .IR global , and are visible from all configuration nodes. They are mostly shell display options, but some starting with .B auto_ affect shell behavior and may merit customization. These include .BR auto_save_on_exit , which controls if exiting targetcli saves the configuration; .BR auto_add_mapped_luns , to automatically add existing LUNs to new ACLs, and new LUNS to existing ACLs; and .BR auto_cd_after_create , to change working path to newly-created nodes. Global settings are user-specific and are saved to ~/.targetcli/ upon exit, unlike other groups, which are system-wide and kept in .BR /etc/target/saveconfig.json . .SS BACKSTORE-SPECIFIC .B attribute .br /backstore// configuration node. Contains values relating to the backstore and storage object. .P .SS ISCSI-SPECIFIC .B discovery_auth .br /iscsi configuration node. Set the normal and mutual authentication userid and password for discovery sessions, as well as enabling or disabling it. By default it is disabled -- no authentication is required for discovery. .P .B parameter .br /iscsi//tpgX configuration node. ISCSI-specific parameters such as .IR AuthMethod , .IR MaxBurstLength , .IR IFMarker , .IR DataDigest , and similar. .P .B attribute .br /iscsi//tpgX configuration node. Contains implementation-specific settings for the TPG, such as .BR authentication , to enforce or disable authentication for the full-feature phase (i.e. non-discovery). .P .B auth .br /iscsi//tpgX/acls/ configuration node. Set the userid and password for full-feature phase for this ACL. .SH FILES .B /etc/target/saveconfig.json .br .B /etc/target/backup/* .SH SEE ALSO .BR targetctl (8), .BR tcmu-runner (8) .SH AUTHOR Written by Jerome Martin . .br Man page written by Andy Grover . .SH REPORTING BUGS Report bugs via .br or targetcli-fb-2.1.fb43/targetcli/000077500000000000000000000000001270157724400164435ustar00rootroot00000000000000targetcli-fb-2.1.fb43/targetcli/__init__.py000066400000000000000000000012321270157724400205520ustar00rootroot00000000000000''' This file is part of targetcli. Copyright (c) 2011-2013 by Datera, Inc Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ''' from .ui_root import UIRoot from .version import __version__ targetcli-fb-2.1.fb43/targetcli/ui_backstore.py000066400000000000000000000543151270157724400214770ustar00rootroot00000000000000''' Implements the targetcli backstores related UI. This file is part of targetcli. Copyright (c) 2011-2013 by Datera, Inc Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ''' import glob import os import re import stat import dbus from configshell_fb import ExecutionError from rtslib_fb import BlockStorageObject, FileIOStorageObject from rtslib_fb import PSCSIStorageObject, RDMCPStorageObject, UserBackedStorageObject from rtslib_fb import RTSLibError from rtslib_fb import RTSRoot from rtslib_fb.utils import get_block_type from .ui_node import UINode, UIRTSLibNode def human_to_bytes(hsize, kilo=1024): ''' This function converts human-readable amounts of bytes to bytes. It understands the following units : - I{B} or no unit present for Bytes - I{k}, I{K}, I{kB}, I{KB} for kB (kilobytes) - I{m}, I{M}, I{mB}, I{MB} for MB (megabytes) - I{g}, I{G}, I{gB}, I{GB} for GB (gigabytes) - I{t}, I{T}, I{tB}, I{TB} for TB (terabytes) Note: The definition of I{kilo} defaults to 1kB = 1024Bytes. Strictly speaking, those should not be called I{kB} but I{kiB}. You can override that with the optional kilo parameter. @param hsize: The human-readable version of the Bytes amount to convert @type hsize: string or int @param kilo: Optional base for the kilo prefix @type kilo: int @return: An int representing the human-readable string converted to bytes ''' size = hsize.replace('i', '') size = size.lower() if not re.match("^[0-9]+[k|m|g|t]?[b]?$", size): raise RTSLibError("Cannot interpret size, wrong format: %s" % hsize) size = size.rstrip('ib') units = ['k', 'm', 'g', 't'] try: power = units.index(size[-1]) + 1 except ValueError: power = 0 size = int(size) else: size = int(size[:-1]) return size * (int(kilo) ** power) def bytes_to_human(size): kilo = 1024.0 # don't use decimal for bytes if size < kilo: return "%d bytes" % size size /= kilo for x in ['KiB', 'MiB', 'GiB', 'TiB', 'PiB']: if size < kilo: return "%3.1f%s" % (size, x) size /= kilo def complete_path(path, stat_fn): filtered = [] for entry in glob.glob(path + '*'): st = os.stat(entry) if stat.S_ISDIR(st.st_mode): filtered.append(entry + '/') elif stat_fn(st.st_mode): filtered.append(entry) # Put directories at the end return sorted(filtered, key=lambda s: '~'+s if s.endswith('/') else s) class UIBackstores(UINode): ''' The backstores container UI. ''' def __init__(self, parent): UINode.__init__(self, 'backstores', parent) self.refresh() def _user_backstores(self): ''' tcmu-runner (or other daemon providing the same service) exposes a DBus ObjectManager-based iface to find handlers it supports. ''' bus = dbus.SystemBus() try: mgr_obj = bus.get_object('org.kernel.TCMUService1', '/org/kernel/TCMUService1') mgr_iface = dbus.Interface(mgr_obj, 'org.freedesktop.DBus.ObjectManager') for k,v in mgr_iface.GetManagedObjects().items(): tcmu_obj = bus.get_object('org.kernel.TCMUService1', k) tcmu_iface = dbus.Interface(tcmu_obj, dbus_interface='org.kernel.TCMUService1') yield (k[k.rfind("/")+1:], tcmu_iface, v) except dbus.DBusException as e: return def refresh(self): self._children = set([]) UIPSCSIBackstore(self) UIRDMCPBackstore(self) UIFileIOBackstore(self) UIBlockBackstore(self) for name, iface, prop_dict in self._user_backstores(): UIUserBackedBackstore(self, name, iface, prop_dict) class UIBackstore(UINode): ''' A backstore UI. Abstract Base Class, do not instantiate. ''' def __init__(self, plugin, parent): UINode.__init__(self, plugin, parent) self.refresh() def refresh(self): self._children = set([]) for so in RTSRoot().storage_objects: if so.plugin == self.name: ui_so = self.so_cls(so, self) def summary(self): return ("Storage Objects: %d" % len(self._children), None) def ui_command_delete(self, name): ''' Recursively deletes the storage object having the specified I{name}. If there are LUNs using this storage object, they will be deleted too. EXAMPLE ======= B{delete mystorage} ------------------- Deletes the storage object named mystorage, and all associated LUNs. ''' self.assert_root() try: child = self.get_child(name) except ValueError: raise ExecutionError("No storage object named %s." % name) child.rtsnode.delete() self.remove_child(child) self.shell.log.info("Deleted storage object %s." % name) def ui_complete_delete(self, parameters, text, current_param): ''' Parameter auto-completion method for user command delete. @param parameters: Parameters on the command line. @type parameters: dict @param text: Current text of parameter being typed by the user. @type text: str @param current_param: Name of parameter to complete. @type current_param: str @return: Possible completions @rtype: list of str ''' if current_param == 'name': names = [child.name for child in self.children] completions = [name for name in names if name.startswith(text)] else: completions = [] if len(completions) == 1: return [completions[0] + ' '] else: return completions def setup_model_alias(self, storageobject): if self.shell.prefs['export_backstore_name_as_model']: try: storageobject.set_attribute("emulate_model_alias", 1) except RTSLibError: raise ExecutionError("'export_backstore_name_as_model' is set but" " emulate_model_alias\n not supported by kernel.") class UIPSCSIBackstore(UIBackstore): ''' PSCSI backstore UI. ''' def __init__(self, parent): self.so_cls = UIPSCSIStorageObject UIBackstore.__init__(self, 'pscsi', parent) def ui_command_create(self, name, dev): ''' Creates a PSCSI storage object, with supplied name and SCSI device. The SCSI device I{dev} can either be a path name to the device, in which case it is recommended to use the /dev/disk/by-id hierarchy to have consistent naming should your physical SCSI system be modified, or an SCSI device ID in the H:C:T:L format, which is not recommended as SCSI IDs may vary in time. ''' self.assert_root() if get_block_type(dev) is not None: self.shell.log.info("Note: block backstore recommended for " "SCSI block devices") so = PSCSIStorageObject(name, dev) ui_so = UIPSCSIStorageObject(so, self) self.shell.log.info("Created pscsi storage object %s using %s" % (name, dev)) return self.new_node(ui_so) class UIRDMCPBackstore(UIBackstore): ''' RDMCP backstore UI. ''' def __init__(self, parent): self.so_cls = UIRamdiskStorageObject UIBackstore.__init__(self, 'ramdisk', parent) def ui_command_create(self, name, size, nullio=None, wwn=None): ''' Creates an RDMCP storage object. I{size} is the size of the ramdisk. SIZE SYNTAX =========== - If size is an int, it represents a number of bytes. - If size is a string, the following units can be used: - B{B} or no unit present for bytes - B{k}, B{K}, B{kB}, B{KB} for kB (kilobytes) - B{m}, B{M}, B{mB}, B{MB} for MB (megabytes) - B{g}, B{G}, B{gB}, B{GB} for GB (gigabytes) - B{t}, B{T}, B{tB}, B{TB} for TB (terabytes) ''' self.assert_root() nullio = self.ui_eval_param(nullio, 'bool', False) wwn = self.ui_eval_param(wwn, 'string', None) so = RDMCPStorageObject(name, human_to_bytes(size), nullio=nullio, wwn=wwn) ui_so = UIRamdiskStorageObject(so, self) self.setup_model_alias(so) self.shell.log.info("Created ramdisk %s with size %s." % (name, size)) return self.new_node(ui_so) class UIFileIOBackstore(UIBackstore): ''' FileIO backstore UI. ''' def __init__(self, parent): self.so_cls = UIFileioStorageObject UIBackstore.__init__(self, 'fileio', parent) def _create_file(self, filename, size, sparse=True): try: f = open(filename, "w+") except (OSError, IOError): raise ExecutionError("Could not open %s" % filename) try: if sparse: try: os.posix_fallocate(f.fileno(), 0, size) except AttributeError: # Prior to version 3.3, Python does not provide fallocate os.ftruncate(f.fileno(), size) else: self.shell.log.info("Writing %d bytes" % size) while size > 0: write_size = min(size, 1024) f.write("\0" * write_size) size -= write_size except (OSError, IOError): os.remove(filename) raise ExecutionError("Could not expand file to %d bytes" % size) except OverflowError: raise ExecutionError("The file size is too large (%d bytes)" % size) finally: f.close() def ui_command_create(self, name, file_or_dev, size=None, write_back=None, sparse=None, wwn=None): ''' Creates a FileIO storage object. If I{file_or_dev} is a path to a regular file to be used as backend, then the I{size} parameter is mandatory. Else, if I{file_or_dev} is a path to a block device, the size parameter B{must} be ommited. If present, I{size} is the size of the file to be used, I{file} the path to the file or I{dev} the path to a block device. The I{write_back} parameter is a boolean controlling write caching. It is enabled by default. The I{sparse} parameter is only applicable when creating a new backing file. It is a boolean stating if the created file should be created as a sparse file (the default), or fully initialized. SIZE SYNTAX =========== - If size is an int, it represents a number of bytes. - If size is a string, the following units can be used: - B{B} or no unit present for bytes - B{k}, B{K}, B{kB}, B{KB} for kB (kilobytes) - B{m}, B{M}, B{mB}, B{MB} for MB (megabytes) - B{g}, B{G}, B{gB}, B{GB} for GB (gigabytes) - B{t}, B{T}, B{tB}, B{TB} for TB (terabytes) ''' self.assert_root() sparse = self.ui_eval_param(sparse, 'bool', True) write_back = self.ui_eval_param(write_back, 'bool', True) wwn = self.ui_eval_param(wwn, 'string', None) self.shell.log.debug("Using params size=%s write_back=%s sparse=%s" % (size, write_back, sparse)) file_or_dev = os.path.expanduser(file_or_dev) # can't use is_dev_in_use() on files so just check against other # storage object paths if os.path.exists(file_or_dev): for so in RTSRoot().storage_objects: if so.udev_path and os.path.samefile(file_or_dev, so.udev_path): raise ExecutionError("storage object for %s already exists: %s" % \ (file_or_dev, so.name)) if get_block_type(file_or_dev) is not None: if size: self.shell.log.info("Block device, size parameter ignored") size = None self.shell.log.info("Note: block backstore preferred for best results") else: # use given file size only if backing file does not exist if os.path.isfile(file_or_dev): new_size = os.path.getsize(file_or_dev) if size: self.shell.log.info("%s exists, using its size (%s bytes) instead" % (file_or_dev, new_size)) size = new_size elif os.path.exists(file_or_dev): raise ExecutionError("Path %s exists but is not a file" % file_or_dev) else: # create file and extend to given file size if not size: raise ExecutionError("Attempting to create file for new" + " fileio backstore, need a size") size = human_to_bytes(size) self._create_file(file_or_dev, size, sparse) so = FileIOStorageObject(name, file_or_dev, size, write_back=write_back, wwn=wwn) ui_so = UIFileioStorageObject(so, self) self.setup_model_alias(so) self.shell.log.info("Created fileio %s with size %s" % (name, so.size)) return self.new_node(ui_so) def ui_complete_create(self, parameters, text, current_param): ''' Auto-completes the file name ''' if current_param != 'file_or_dev': return [] completions = complete_path(text, lambda x: stat.S_ISREG(x) or stat.S_ISBLK(x)) if len(completions) == 1 and not completions[0].endswith('/'): completions = [completions[0] + ' '] return completions class UIBlockBackstore(UIBackstore): ''' Block backstore UI. ''' def __init__(self, parent): self.so_cls = UIBlockStorageObject UIBackstore.__init__(self, 'block', parent) def ui_command_create(self, name, dev, readonly=None, wwn=None): ''' Creates an Block Storage object. I{dev} is the path to the TYPE_DISK block device to use. ''' self.assert_root() readonly = self.ui_eval_param(readonly, 'bool', False) wwn = self.ui_eval_param(wwn, 'string', None) so = BlockStorageObject(name, dev, readonly=readonly, wwn=wwn) ui_so = UIBlockStorageObject(so, self) self.setup_model_alias(so) self.shell.log.info("Created block storage object %s using %s." % (name, dev)) return self.new_node(ui_so) def ui_complete_create(self, parameters, text, current_param): ''' Auto-completes the device name ''' if current_param != 'dev': return [] completions = complete_path(text, stat.S_ISBLK) if len(completions) == 1 and not completions[0].endswith('/'): completions = [completions[0] + ' '] return completions class UIUserBackedBackstore(UIBackstore): ''' User backstore UI. ''' def __init__(self, parent, name, iface, prop_dict): self.so_cls = UIUserBackedStorageObject self.handler = name self.iface = iface self.prop_dict = prop_dict super(UIUserBackedBackstore, self).__init__("user:"+name, parent) def refresh(self): self._children = set([]) for so in RTSRoot().storage_objects: if so.plugin == 'user': idx = so.config.find("/") handler = so.config[:idx] if handler == self.handler: ui_so = self.so_cls(so, self) def ui_command_help(self, topic=None): super(UIUserBackedBackstore, self).ui_command_help(topic) if topic == "create": print("CFGSTRING FORMAT") print("=================") x = self.prop_dict.get("org.kernel.TCMUService1", {}) print(x.get("ConfigDesc", "No description.")) print() def ui_command_create(self, name, size, cfgstring, wwn=None): ''' Creates a User-backed storage object. SIZE SYNTAX =========== - If size is an int, it represents a number of bytes. - If size is a string, the following units can be used: - B{B} or no unit present for bytes - B{k}, B{K}, B{kB}, B{KB} for kB (kilobytes) - B{m}, B{M}, B{mB}, B{MB} for MB (megabytes) - B{g}, B{G}, B{gB}, B{GB} for GB (gigabytes) - B{t}, B{T}, B{tB}, B{TB} for TB (terabytes) ''' size = human_to_bytes(size) wwn = self.ui_eval_param(wwn, 'string', None) config = self.handler + "/" + cfgstring ok, errmsg = self.iface.CheckConfig(config) if not ok: raise ExecutionError("cfgstring invalid: %s" % errmsg) so = UserBackedStorageObject(name, size=size, config=config, wwn=wwn) ui_so = UIUserBackedStorageObject(so, self) self.shell.log.info("Created user-backed storage object %s size %d." % (name, size)) return self.new_node(ui_so) class UIStorageObject(UIRTSLibNode): ''' A storage object UI. Abstract Base Class, do not instantiate. ''' ui_desc_attributes = { 'block_size': ('number', 'Block size of the underlying device.'), 'emulate_3pc': ('number', 'If set to 1, enable Third Party Copy.'), 'emulate_caw': ('number', 'If set to 1, enable Compare and Write.'), 'emulate_dpo': ('number', 'If set to 1, turn on Disable Page Out.'), 'emulate_fua_read': ('number', 'If set to 1, enable Force Unit Access read.'), 'emulate_fua_write': ('number', 'If set to 1, enable Force Unit Access write.'), 'emulate_model_alias': ('number', 'If set to 1, use the backend device name for the model alias.'), 'emulate_rest_reord': ('number', 'If set to 0, the Queue Algorithm Modifier is Restricted Reordering.'), 'emulate_tas': ('number', 'If set to 1, enable Task Aborted Status.'), 'emulate_tpu': ('number', 'If set to 1, enable Thin Provisioning Unmap.'), 'emulate_tpws': ('number', 'If set to 1, enable Thin Provisioning Write Same.'), 'emulate_ua_intlck_ctrl': ('number', 'If set to 1, enable Unit Attention Interlock.'), 'emulate_write_cache': ('number', 'If set to 1, turn on Write Cache Enable.'), 'enforce_pr_isids': ('number', 'If set to 1, enforce persistent reservation ISIDs.'), 'force_pr_aptpl': ('number', 'If set to 1, force SPC-3 PR Activate Persistence across Target Power Loss operation.'), 'fabric_max_sectors': ('number', 'Maximum number of sectors the fabric can transfer at once.'), 'hw_block_size': ('number', 'Hardware block size in bytes.'), 'hw_max_sectors': ('number', 'Maximum number of sectors the hardware can transfer at once.'), 'hw_pi_prot_type': ('number', 'If non-zero, DIF protection is enabled on the underlying hardware.'), 'hw_queue_depth': ('number', 'Hardware queue depth.'), 'is_nonrot': ('number', 'If set to 1, the backstore is a non rotational device.'), 'max_unmap_block_desc_count': ('number', 'Maximum number of block descriptors for UNMAP.'), 'max_unmap_lba_count': ('number', 'Maximum number of LBA for UNMAP.'), 'max_write_same_len': ('number', 'Maximum length for WRITE_SAME.'), 'optimal_sectors': ('number', 'Optimal request size in sectors.'), 'pi_prot_format': ('number', 'DIF protection format.'), 'pi_prot_type': ('number', 'DIF protection type.'), 'queue_depth': ('number', 'Queue depth.'), 'unmap_granularity': ('number', 'UNMAP granularity.'), 'unmap_granularity_alignment': ('number', 'UNMAP granularity alignment.'), 'unmap_zeroes_data': ('number', 'If set to 1, zeroes are read back after an UNMAP.'), } def __init__(self, storage_object, parent): name = storage_object.name UIRTSLibNode.__init__(self, name, storage_object, parent) self.refresh() def ui_command_version(self): ''' Displays the version of the current backstore's plugin. ''' self.shell.con.display("Backstore plugin %s %s" % (self.rtsnode.plugin, self.rtsnode.version)) class UIPSCSIStorageObject(UIStorageObject): def summary(self): so = self.rtsnode return ("%s %s" % (so.udev_path, so.status), True) class UIRamdiskStorageObject(UIStorageObject): def summary(self): so = self.rtsnode nullio_str = "" if so.nullio: nullio_str = "nullio " return ("%s(%s) %s" % (nullio_str, bytes_to_human(so.size), so.status), True) class UIFileioStorageObject(UIStorageObject): def summary(self): so = self.rtsnode if so.write_back: wb_str = "write-back" else: wb_str = "write-thru" return ("%s (%s) %s %s" % (so.udev_path, bytes_to_human(so.size), wb_str, so.status), True) class UIBlockStorageObject(UIStorageObject): def summary(self): so = self.rtsnode if so.write_back: wb_str = "write-back" else: wb_str = "write-thru" ro_str = "" if so.readonly: ro_str = "ro " return ("%s (%s) %s%s %s" % (so.udev_path, bytes_to_human(so.size), ro_str, wb_str, so.status), True) class UIUserBackedStorageObject(UIStorageObject): def summary(self): so = self.rtsnode if not so.config: config_str = "(no config)" else: idx = so.config.find("/") config_str = so.config[idx+1:] return ("%s (%s) %s" % (config_str, bytes_to_human(so.size), so.status), True) targetcli-fb-2.1.fb43/targetcli/ui_node.py000066400000000000000000000171121270157724400204410ustar00rootroot00000000000000''' Implements the targetcli base UI node. This file is part of targetcli. Copyright (c) 2011-2013 by Datera, Inc Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ''' import six from configshell_fb import ConfigNode, ExecutionError class UINode(ConfigNode): ''' Our targetcli basic UI node. ''' def __init__(self, name, parent=None, shell=None): ConfigNode.__init__(self, name, parent, shell) self.define_config_group_param( 'global', 'export_backstore_name_as_model', 'bool', 'If true, the backstore name is used for the scsi inquiry model name.') self.define_config_group_param( 'global', 'auto_enable_tpgt', 'bool', 'If true, automatically enables TPGTs upon creation.') self.define_config_group_param( 'global', 'auto_add_mapped_luns', 'bool', 'If true, automatically create node ACLs mapped LUNs ' + 'after creating a new target LUN or a new node ACL') self.define_config_group_param( 'global', 'auto_cd_after_create', 'bool', 'If true, changes current path to newly created objects.') self.define_config_group_param( 'global', 'auto_save_on_exit', 'bool', 'If true, saves configuration on exit.') self.define_config_group_param( 'global', 'auto_add_default_portal', 'bool', 'If true, adds a portal listening on all IPs to new targets.') def assert_root(self): ''' For commands requiring root privileges, disable command if not the root node's as_root attribute is False. ''' root_node = self.get_root() if hasattr(root_node, 'as_root') and not self.get_root().as_root: raise ExecutionError("This privileged command is disabled: " + "you are not root.") def new_node(self, new_node): ''' Used to honor global 'auto_cd_after_create'. Either returns None if the global is False, or the new_node if the global is True. In both cases, set the @last bookmark to last_node. ''' self.shell.prefs['bookmarks']['last'] = new_node.path self.shell.prefs.save() if self.shell.prefs['auto_cd_after_create']: self.shell.log.info("Entering new node %s" % new_node.path) # Piggy backs on cd instead of just returning new_node, # so we update navigation history. return self.ui_command_cd(new_node.path) else: return None def refresh(self): ''' Refreshes and updates the objects tree from the current path. ''' for child in self.children: child.refresh() def ui_command_refresh(self): ''' Refreshes and updates the objects tree from the current path. ''' self.refresh() def ui_command_status(self): ''' Displays the current node's status summary. SEE ALSO ======== B{ls} ''' description, is_healthy = self.summary() self.shell.log.info("Status for %s: %s" % (self.path, description)) def ui_setgroup_global(self, parameter, value): ConfigNode.ui_setgroup_global(self, parameter, value) self.get_root().refresh() def ui_type_yesno(self, value=None, enum=False, reverse=False): ''' UI parameter type helper for "Yes" and "No" boolean values. "Yes" and "No" are used for boolean iSCSI session parameters. ''' if reverse: if value is not None: return value else: return 'n/a' type_enum = ('Yes', 'No') syntax = '|'.join(type_enum) if value is None: if enum: return type_enum else: return syntax elif value in type_enum: return value else: raise ValueError("Syntax error, '%s' is not %s." % (value, syntax)) class UIRTSLibNode(UINode): ''' A subclass of UINode for nodes with an underlying RTSLib object. ''' def __init__(self, name, rtslib_object, parent, late_params=False): ''' Call from the class that inherits this, with the rtslib object that should be checked upon. ''' UINode.__init__(self, name, parent) self.rtsnode = rtslib_object if late_params: return # If the rtsnode has parameters, use them parameters = self.rtsnode.list_parameters() parameters_ro = self.rtsnode.list_parameters(writable=False) for parameter in parameters: writable = parameter not in parameters_ro type, desc = getattr(self.__class__, 'ui_desc_parameters', {}).get(parameter, ('string', '')) self.define_config_group_param( 'parameter', parameter, type, desc, writable) # If the rtsnode has attributes, enable them attributes = self.rtsnode.list_attributes() attributes_ro = self.rtsnode.list_attributes(writable=False) for attribute in attributes: writable = attribute not in attributes_ro type, desc = getattr(self.__class__, 'ui_desc_attributes', {}).get(attribute, ('string', '')) self.define_config_group_param( 'attribute', attribute, type, desc, writable) def ui_getgroup_attribute(self, attribute): ''' This is the backend method for getting attributes. @param attribute: The attribute to get the value of. @type attribute: str @return: The attribute's value @rtype: arbitrary ''' return self.rtsnode.get_attribute(attribute) def ui_setgroup_attribute(self, attribute, value): ''' This is the backend method for setting attributes. @param attribute: The attribute to set the value of. @type attribute: str @param value: The attribute's value @type value: arbitrary ''' self.assert_root() self.rtsnode.set_attribute(attribute, value) def ui_getgroup_parameter(self, parameter): ''' This is the backend method for getting parameters. @param parameter: The parameter to get the value of. @type parameter: str @return: The parameter's value @rtype: arbitrary ''' return self.rtsnode.get_parameter(parameter) def ui_setgroup_parameter(self, parameter, value): ''' This is the backend method for setting parameters. @param parameter: The parameter to set the value of. @type parameter: str @param value: The parameter's value @type value: arbitrary ''' self.assert_root() self.rtsnode.set_parameter(parameter, value) def ui_command_info(self): info = self.rtsnode.dump() for item in ('attributes', 'parameters'): if item in info: del info[item] for name, value in sorted(six.iteritems(info)): if not isinstance (value, (dict, list)): self.shell.log.info("%s: %s" % (name, value)) targetcli-fb-2.1.fb43/targetcli/ui_root.py000066400000000000000000000170401270157724400204770ustar00rootroot00000000000000''' Implements the targetcli root UI. This file is part of targetcli. Copyright (c) 2011-2013 by Datera, Inc Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ''' from datetime import datetime from glob import glob import os import shutil import stat from configshell_fb import ExecutionError from rtslib_fb import RTSRoot from rtslib_fb.utils import ignored from .ui_backstore import complete_path, UIBackstores from .ui_node import UINode from .ui_target import UIFabricModule default_save_file = "/etc/target/saveconfig.json" kept_backups = 10 class UIRoot(UINode): ''' The targetcli hierarchy root node. ''' def __init__(self, shell, as_root=False): UINode.__init__(self, '/', shell=shell) self.as_root = as_root self.rtsroot = RTSRoot() def refresh(self): ''' Refreshes the tree of target fabric modules. ''' self._children = set([]) UIBackstores(self) # only show fabrics present in the system for fm in self.rtsroot.fabric_modules: if fm.wwns == None or any(fm.wwns): UIFabricModule(fm, self) def ui_command_saveconfig(self, savefile=default_save_file): ''' Saves the current configuration to a file so that it can be restored on next boot. ''' self.assert_root() savefile = os.path.expanduser(savefile) # Only save backups if saving to default location if savefile == default_save_file: backup_dir = os.path.dirname(savefile) + "/backup" backup_name = "saveconfig-" + \ datetime.now().strftime("%Y%m%d-%H:%M:%S") + ".json" backupfile = backup_dir + "/" + backup_name with ignored(IOError): shutil.copy(savefile, backupfile) # Kill excess backups backups = sorted(glob(os.path.dirname(savefile) + "/backup/*.json")) files_to_unlink = list(reversed(backups))[kept_backups:] for f in files_to_unlink: os.unlink(f) self.shell.log.info("Last %d configs saved in %s." % \ (kept_backups, backup_dir)) self.rtsroot.save_to_file(savefile) self.shell.log.info("Configuration saved to %s" % savefile) def ui_command_restoreconfig(self, savefile=default_save_file, clear_existing=False): ''' Restores configuration from a file. ''' self.assert_root() savefile = os.path.expanduser(savefile) if not os.path.isfile(savefile): self.shell.log.info("Restore file %s not found" % savefile) return errors = self.rtsroot.restore_from_file(savefile, clear_existing) self.refresh() if errors: raise ExecutionError("Configuration restored, %d recoverable errors:\n%s" % \ (len(errors), "\n".join(errors))) self.shell.log.info("Configuration restored from %s" % savefile) def ui_complete_saveconfig(self, parameters, text, current_param): ''' Auto-completes the file name ''' if current_param != 'savefile': return [] completions = complete_path(text, stat.S_ISREG) if len(completions) == 1 and not completions[0].endswith('/'): completions = [completions[0] + ' '] return completions ui_complete_restoreconfig = ui_complete_saveconfig def ui_command_clearconfig(self, confirm=None): ''' Removes entire configuration of backstores and targets ''' self.assert_root() confirm = self.ui_eval_param(confirm, 'bool', False) self.rtsroot.clear_existing(confirm=confirm) self.shell.log.info("All configuration cleared") self.refresh() def ui_command_version(self): ''' Displays the targetcli and support libraries versions. ''' from targetcli import __version__ as targetcli_version self.shell.log.info("targetcli version %s" % targetcli_version) def ui_command_sessions(self, action="list", sid=None): ''' Displays a detailed list of all open sessions. PARAMETERS ========== I{action} --------- The I{action} is one of: - B{list} gives a short session list - B{detail} gives a detailed list I{sid} ------ You can specify an I{sid} to only list this one, with or without details. SEE ALSO ======== status ''' indent_step = 4 base_steps = 0 action_list = ("list", "detail") if action not in action_list: raise ExecutionError("action must be one of: %s" % ", ".join(action_list)) if sid is not None: try: int(sid) except ValueError: raise ExecutionError("sid must be a number, '%s' given" % sid) def indent_print(text, steps): console = self.shell.con console.display(console.indent(text, indent_step * steps), no_lf=True) def print_session(session): acl = session['parent_nodeacl'] indent_print("alias: %(alias)s\tsid: %(id)i type: " \ "%(type)s session-state: %(state)s" % session, base_steps) if action == 'detail': if self.as_root: if acl.authenticate_target: auth = " (authenticated)" else: auth = " (NOT AUTHENTICATED)" else: auth = "" indent_print("name: %s%s" % (acl.node_wwn, auth), base_steps + 1) for mlun in acl.mapped_luns: plugin = mlun.tpg_lun.storage_object.plugin name = mlun.tpg_lun.storage_object.name if mlun.write_protect: mode = "r" else: mode = "rw" indent_print("mapped-lun: %d backstore: %s/%s mode: %s" % (mlun.mapped_lun, plugin, name, mode), base_steps + 1) for connection in session['connections']: indent_print("address: %(address)s (%(transport)s) cid: " \ "%(cid)i connection-state: %(cstate)s" % \ connection, base_steps + 1) if sid: printed_sessions = [x for x in self.rtsroot.sessions if x['id'] == int(sid)] else: printed_sessions = list(self.rtsroot.sessions) if len(printed_sessions): for session in printed_sessions: print_session(session) else: if sid is None: indent_print("(no open sessions)", base_steps) else: raise ExecutionError("no session found with sid %i" % int(sid)) targetcli-fb-2.1.fb43/targetcli/ui_target.py000066400000000000000000001476141270157724400210150ustar00rootroot00000000000000''' Implements the targetcli target related UI. This file is part of targetcli. Copyright (c) 2011-2013 by Datera, Inc Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ''' try: import ethtool except ImportError: ethtool = None import os import six import stat from configshell_fb import ExecutionError from rtslib_fb import RTSLibBrokenLink, RTSLibError from rtslib_fb import MappedLUN, NetworkPortal, NodeACL from rtslib_fb import LUN, Target, TPG, StorageObjectFactory from .ui_backstore import complete_path from .ui_node import UINode, UIRTSLibNode auth_params = ('userid', 'password', 'mutual_userid', 'mutual_password') discovery_params = auth_params + ("enable",) class UIFabricModule(UIRTSLibNode): ''' A fabric module UI. ''' def __init__(self, fabric_module, parent): super(UIFabricModule, self).__init__(fabric_module.name, fabric_module, parent, late_params=True) self.refresh() if self.rtsnode.has_feature('discovery_auth'): for param in discovery_params: self.define_config_group_param('discovery_auth', param, 'string') self.refresh() # Support late params # # By default the base class will call list_parameters and list_attributes # in init. This stops us from being able to lazy-load fabric modules. # We declare we support "late_params" to stop this, and then # this code overrides the base class methods that involve enumerating # this stuff, so we don't need to call list_parameters/attrs (which # would cause the module to load) until the ui is actually asking for # them from us. # Currently fabricmodules don't have these anyways, this is all a CYA thing. def list_config_groups(self): groups = super(UIFabricModule, self).list_config_groups() if len(self.rtsnode.list_parameters()): groups.append('parameter') if len(self.rtsnode.list_attributes()): groups.append('attribute') return groups # Support late params (see above) def list_group_params(self, group, writable=None): if group not in ("parameter", "attribute"): return super(UIFabricModule, self).list_group_params(group, writable) params_func = getattr(self.rtsnode, "list_%ss" % group) params = params_func() params_ro = params_func(writable=False) ret_list = [] for param in params: p_writable = param not in params_ro if writable is not None and p_writable != writable: continue ret_list.append(param) ret_list.sort() return ret_list # Support late params (see above) def get_group_param(self, group, param): if group not in ("parameter", "attribute"): return super(UIFabricModule, self).get_group_param(group, param) if param not in self.list_group_params(group): raise ValueError("Not such parameter %s in configuration group %s" % (param, group)) description = "The %s %s." % (param, group) writable = param in self.list_group_params(group, writable=True) return dict(name=param, group=group, type="string", description=description, writable=writable) def ui_getgroup_discovery_auth(self, auth_attr): ''' This is the backend method for getting discovery_auth attributes. @param auth_attr: The auth attribute to get the value of. @type auth_attr: str @return: The auth attribute's value @rtype: str ''' if auth_attr == 'enable': return self.rtsnode.discovery_enable_auth else: return getattr(self.rtsnode, "discovery_" + auth_attr) def ui_setgroup_discovery_auth(self, auth_attr, value): ''' This is the backend method for setting discovery auth attributes. @param auth_attr: The auth attribute to set the value of. @type auth_attr: str @param value: The auth's value @type value: str ''' self.assert_root() if value is None: value = '' if auth_attr == 'enable': self.rtsnode.discovery_enable_auth = value else: setattr(self.rtsnode, "discovery_" + auth_attr, value) def refresh(self): self._children = set([]) for target in self.rtsnode.targets: self.shell.log.debug("Found target %s under fabric module %s." % (target.wwn, target.fabric_module)) if target.has_feature('tpgts'): UIMultiTPGTarget(target, self) else: UITarget(target, self) def summary(self): status = None msg = [] fm = self.rtsnode if fm.has_feature('discovery_auth') and fm.discovery_enable_auth: if not (fm.discovery_password and fm.discovery_userid): status = False else: status = True if fm.discovery_authenticate_target: msg.append("mutual disc auth") else: msg.append("1-way disc auth") msg.append("Targets: %d" % len(self._children)) return (", ".join(msg), status) def ui_command_create(self, wwn=None): ''' Creates a new target. The I{wwn} format depends on the transport(s) supported by the fabric module. If the I{wwn} is ommited, then a target will be created using either a randomly generated WWN of the proper type, or the first unused WWN in the list of possible WWNs if one is available. If WWNs are constrained to a list (i.e. for hardware targets addresses) and all WWNs are in use, the target creation will fail. Use the B{info} command to get more information abour WWN type and possible values. SEE ALSO ======== B{info} ''' self.assert_root() target = Target(self.rtsnode, wwn, mode='create') wwn = target.wwn if self.rtsnode.wwns != None and wwn not in self.rtsnode.wwns: self.shell.log.warning("Hardware missing for this WWN") if target.has_feature('tpgts'): ui_target = UIMultiTPGTarget(target, self) self.shell.log.info("Created target %s." % wwn) return ui_target.ui_command_create() else: ui_target = UITarget(target, self) self.shell.log.info("Created target %s." % wwn) return self.new_node(ui_target) def ui_complete_create(self, parameters, text, current_param): ''' Parameter auto-completion method for user command create. @param parameters: Parameters on the command line. @type parameters: dict @param text: Current text of parameter being typed by the user. @type text: str @param current_param: Name of parameter to complete. @type current_param: str @return: Possible completions @rtype: list of str ''' if current_param == 'wwn' and self.rtsnode.wwns is not None: existing_wwns = [child.wwn for child in self.rtsnode.targets] completions = [wwn for wwn in self.rtsnode.wwns if wwn.startswith(text) if wwn not in existing_wwns] else: completions = [] if len(completions) == 1: return [completions[0] + ' '] else: return completions def ui_command_delete(self, wwn): ''' Recursively deletes the target with the specified I{wwn}, and all objects hanging under it. SEE ALSO ======== B{create} ''' self.assert_root() target = Target(self.rtsnode, wwn, mode='lookup') target.delete() self.shell.log.info("Deleted Target %s." % wwn) self.refresh() def ui_complete_delete(self, parameters, text, current_param): ''' Parameter auto-completion method for user command delete. @param parameters: Parameters on the command line. @type parameters: dict @param text: Current text of parameter being typed by the user. @type text: str @param current_param: Name of parameter to complete. @type current_param: str @return: Possible completions @rtype: list of str ''' if current_param == 'wwn': wwns = [child.name for child in self.children] completions = [wwn for wwn in wwns if wwn.startswith(text)] else: completions = [] if len(completions) == 1: return [completions[0] + ' '] else: return completions def ui_command_info(self): ''' Displays information about the fabric module, notably the supported transports(s) and accepted B{wwn} format(s), as long as supported features. ''' fabric = self.rtsnode self.shell.log.info("Fabric module name: %s" % self.name) self.shell.log.info("ConfigFS path: %s" % self.rtsnode.path) self.shell.log.info("Allowed WWN types: %s" % ", ".join(fabric.wwn_types)) if fabric.wwns is not None: self.shell.log.info("Allowed WWNs list: %s" % ', '.join(fabric.wwns)) self.shell.log.info("Fabric module features: %s" % ', '.join(fabric.features)) self.shell.log.info("Corresponding kernel module: %s" % fabric.kernel_module) def ui_command_version(self): ''' Displays the target fabric module version. ''' version = "Target fabric module %s: %s" \ % (self.rtsnode.name, self.rtsnode.version) self.shell.con.display(version.strip()) class UIMultiTPGTarget(UIRTSLibNode): ''' A generic target UI that has multiple TPGs. ''' def __init__(self, target, parent): super(UIMultiTPGTarget, self).__init__(target.wwn, target, parent) self.refresh() def refresh(self): self._children = set([]) for tpg in self.rtsnode.tpgs: UITPG(tpg, self) def summary(self): try: self.rtsnode.fabric_module.to_normalized_wwn(self.rtsnode.wwn) except: return ("INVALID WWN", False) return ("TPGs: %d" % len(self._children), None) def ui_command_create(self, tag=None): ''' Creates a new Target Portal Group within the target. The I{tag} must be a positive integer value, optionally prefaced by 'tpg'. If omitted, the next available Target Portal Group Tag (TPGT) will be used. SEE ALSO ======== B{delete} ''' self.assert_root() if tag: if tag.startswith("tpg"): tag = tag[3:] try: tag = int(tag) except ValueError: raise ExecutionError("Tag argument must be a number.") tpg = TPG(self.rtsnode, tag, mode='create') if self.shell.prefs['auto_enable_tpgt']: tpg.enable = True if tpg.has_feature("auth"): tpg.set_attribute("authentication", 0) self.shell.log.info("Created TPG %s." % tpg.tag) if tpg.has_feature("nps") and self.shell.prefs['auto_add_default_portal']: try: NetworkPortal(tpg, "0.0.0.0") self.shell.log.info("Global pref auto_add_default_portal=true") self.shell.log.info("Created default portal listening on all IPs" " (0.0.0.0), port 3260.") except RTSLibError: self.shell.log.info("Default portal not created, TPGs within a " + "target cannot share ip:port.") ui_tpg = UITPG(tpg, self) return self.new_node(ui_tpg) def ui_command_delete(self, tag): ''' Deletes the Target Portal Group with TPGT I{tag} from the target. The I{tag} must be a positive integer matching an existing TPGT. SEE ALSO ======== B{create} ''' self.assert_root() if tag.startswith("tpg"): tag = tag[3:] try: tag = int(tag) except ValueError: raise ExecutionError("Tag argument must be a number.") tpg = TPG(self.rtsnode, tag, mode='lookup') tpg.delete() self.shell.log.info("Deleted TPGT %s." % tag) self.refresh() def ui_complete_delete(self, parameters, text, current_param): ''' Parameter auto-completion method for user command delete. @param parameters: Parameters on the command line. @type parameters: dict @param text: Current text of parameter being typed by the user. @type text: str @param current_param: Name of parameter to complete. @type current_param: str @return: Possible completions @rtype: list of str ''' if current_param == 'tag': tags = [child.name[4:] for child in self.children] completions = [tag for tag in tags if tag.startswith(text)] else: completions = [] if len(completions) == 1: return [completions[0] + ' '] else: return completions class UITPG(UIRTSLibNode): ui_desc_attributes = { 'authentication': ('number', 'If set to 1, enforce authentication for this TPG.'), 'cache_dynamic_acls': ('number', 'If set to 1 in demo mode, cache dynamically generated ACLs.'), 'default_cmdsn_depth': ('number', 'Default CmdSN (Command Sequence Number) depth.'), 'default_erl': ('number', 'Default Error Recovery Level.'), 'demo_mode_discovery': ('number', 'If set to 1 in demo mode, enable discovery.'), 'demo_mode_write_protect': ('number', 'If set to 1 in demo mode, prevent writes to LUNs.'), 'fabric_prot_type': ('number', 'Fabric DIF protection type.'), 'generate_node_acls': ('number', 'If set to 1, allow all initiators to login (i.e. demo mode).'), 'login_timeout': ('number', 'Login timeout value in seconds.'), 'netif_timeout': ('number', 'NIC failure timeout in seconds.'), 'prod_mode_write_protect': ('number', 'If set to 1, prevent writes to LUNs.'), 't10_pi': ('number', 'If set to 1, enable T10 Protection Information.'), 'tpg_enabled_sendtargets': ('number', 'If set to 1, the SendTargets discovery response advertises the TPG only if the TPG is enabled.'), } ui_desc_parameters = { 'AuthMethod': ('string', 'Authentication method used by the TPG.'), 'DataDigest': ('string', 'If set to CRC32C, the integrity of the PDU data part is verified.'), 'DataPDUInOrder': ('yesno', 'If set to Yes, the data PDUs within sequences must be in order.'), 'DataSequenceInOrder': ('yesno', 'If set to Yes, the data sequences must be in order.'), 'DefaultTime2Retain': ('number', 'Maximum time, in seconds, after an initial wait, before which an active task reassignment is still possible after an unexpected connection termination or a connection reset.'), 'DefaultTime2Wait': ('number', 'Minimum time, in seconds, to wait before attempting an explicit/implicit logout or an active task reassignment after an unexpected connection termination or a connection reset.'), 'ErrorRecoveryLevel': ('number', 'Recovery levels represent a combination of recovery capabilities.'), 'FirstBurstLength': ('number', 'Maximum amount in bytes of unsolicited data an initiator may send.'), 'HeaderDigest': ('string', 'If set to CRC32C, the integrity of the PDU header part is verified.'), 'IFMarker': ('yesno', 'Deprecated according to RFC 7143.'), 'IFMarkInt': ('string', 'Deprecated according to RFC 7143.'), 'ImmediateData': ('string', 'Immediate data support.'), 'InitialR2T': ('yesno', 'If set to No, the default use of R2T (Ready To Transfer) is disabled.'), 'MaxBurstLength': ('number', 'Maximum SCSI data payload in bytes in a Data-In or a solicited Data-Out iSCSI sequence.'), 'MaxConnections': ('number', 'Maximum number of connections acceptable.'), 'MaxOutstandingR2T': ('number', 'Maximum number of outstanding R2Ts per task.'), 'MaxRecvDataSegmentLength': ('number', 'Maximum data segment length in bytes the target can receive in an iSCSI PDU.'), 'MaxXmitDataSegmentLength': ('number', 'Outgoing MaxRecvDataSegmentLength sent over the wire during iSCSI login response.'), 'OFMarker': ('yesno', 'Deprecated according to RFC 7143.'), 'OFMarkInt': ('string', 'Deprecated according to RFC 7143.'), 'TargetAlias': ('string', 'Human-readable target name or description.'), } ''' A generic TPG UI. ''' def __init__(self, tpg, parent): name = "tpg%d" % tpg.tag super(UITPG, self).__init__(name, tpg, parent) self.refresh() UILUNs(tpg, self) if tpg.has_feature('acls'): UINodeACLs(self.rtsnode, self) if tpg.has_feature('nps'): UIPortals(self.rtsnode, self) if self.rtsnode.has_feature('auth') \ and os.path.exists(self.rtsnode.path + "/auth"): for param in auth_params: self.define_config_group_param('auth', param, 'string') def summary(self): tpg = self.rtsnode status = None msg = [] if tpg.has_feature('nexus'): msg.append(str(self.rtsnode.nexus)) if not tpg.enable: return ("disabled", False) if tpg.has_feature("acls"): if "generate_node_acls" in tpg.list_attributes() and \ int(tpg.get_attribute("generate_node_acls")): msg.append("gen-acls") else: msg.append("no-gen-acls") # 'auth' feature requires 'acls' if tpg.has_feature("auth"): if not int(tpg.get_attribute("authentication")): msg.append("no-auth") if int(tpg.get_attribute("generate_node_acls")): # if auth=0, g_n_a=1 is recommended status = True else: if not int(tpg.get_attribute("generate_node_acls")): msg.append("auth per-acl") else: msg.append("tpg-auth") status = True if not (tpg.chap_password and tpg.chap_userid): status = False if tpg.authenticate_target: msg.append("mutual auth") else: msg.append("1-way auth") return (", ".join(msg), status) def ui_getgroup_auth(self, auth_attr): return getattr(self.rtsnode, "chap_" + auth_attr) def ui_setgroup_auth(self, auth_attr, value): self.assert_root() if value is None: value = '' setattr(self.rtsnode, "chap_" + auth_attr, value) def ui_command_enable(self): ''' Enables the TPG. SEE ALSO ======== B{disable status} ''' self.assert_root() if self.rtsnode.enable: self.shell.log.info("The TPGT is already enabled.") else: try: self.rtsnode.enable = True self.shell.log.info("The TPGT has been enabled.") except RTSLibError: raise ExecutionError("The TPGT could not be enabled.") def ui_command_disable(self): ''' Disables the TPG. SEE ALSO ======== B{enable status} ''' self.assert_root() if self.rtsnode.enable: self.rtsnode.enable = False self.shell.log.info("The TPGT has been disabled.") else: self.shell.log.info("The TPGT is already disabled.") class UITarget(UITPG): ''' A generic target UI merged with its only TPG. ''' def __init__(self, target, parent): super(UITarget, self).__init__(TPG(target, 1), parent) self._name = target.wwn self.target = target if self.parent.name != "sbp": self.rtsnode.enable = True def summary(self): try: self.target.fabric_module.to_normalized_wwn(self.target.wwn) except: return ("INVALID WWN", False) return super(UITarget, self).summary() class UINodeACLs(UINode): ''' A generic UI for node ACLs. ''' def __init__(self, tpg, parent): super(UINodeACLs, self).__init__("acls", parent) self.tpg = tpg self.refresh() def refresh(self): self._children = set([]) for name in self.all_names(): UINodeACL(name, self) def summary(self): return ("ACLs: %d" % len(self._children), None) def ui_command_create(self, wwn, add_mapped_luns=None): ''' Creates a Node ACL for the initiator node with the specified I{wwn}. The node's I{wwn} must match the expected WWN Type of the target's fabric module. If I{add_mapped_luns} is omitted, the global parameter B{auto_add_mapped_luns} will be used, else B{true} or B{false} are accepted. If B{true}, then after creating the ACL, mapped LUNs will be automatically created for all existing LUNs. SEE ALSO ======== B{delete} ''' self.assert_root() add_mapped_luns = self.ui_eval_param(add_mapped_luns, 'bool', self.shell.prefs['auto_add_mapped_luns']) node_acl = NodeACL(self.tpg, wwn, mode="create") ui_node_acl = UINodeACL(node_acl.node_wwn, self) self.shell.log.info("Created Node ACL for %s" % node_acl.node_wwn) if add_mapped_luns: for lun in self.tpg.luns: MappedLUN(node_acl, lun.lun, lun.lun, write_protect=False) self.shell.log.info("Created mapped LUN %d." % lun.lun) self.refresh() return self.new_node(ui_node_acl) def ui_command_delete(self, wwn): ''' Deletes the Node ACL with the specified I{wwn}. SEE ALSO ======== B{create} ''' self.assert_root() node_acl = NodeACL(self.tpg, wwn, mode='lookup') node_acl.delete() self.shell.log.info("Deleted Node ACL %s." % wwn) self.refresh() def ui_complete_delete(self, parameters, text, current_param): ''' Parameter auto-completion method for user command delete. @param parameters: Parameters on the command line. @type parameters: dict @param text: Current text of parameter being typed by the user. @type text: str @param current_param: Name of parameter to complete. @type current_param: str @return: Possible completions @rtype: list of str ''' if current_param == 'wwn': wwns = [acl.node_wwn for acl in self.tpg.node_acls] completions = [wwn for wwn in wwns if wwn.startswith(text)] else: completions = [] if len(completions) == 1: return [completions[0] + ' '] else: return completions def find_tagged(self, name): for na in self.tpg.node_acls: if na.node_wwn == name: yield na elif na.tag == name: yield na def all_names(self): names = set([]) for na in self.tpg.node_acls: if na.tag: names.add(na.tag) else: names.add(na.node_wwn) return names def ui_command_tag(self, wwn_or_tag, new_tag): ''' Tag a NodeACL. Usage: tag Tags help manage initiator WWNs. A tag can apply to one or more WWNs. This can give a more meaningful name to a single initiator's configuration, or allow multiple initiators with identical settings to be configured en masse. The WWNs described by will be given the new tag. If new_tag already exists, its new members will adopt the current tag's configuration. Within a tag, the 'info' command shows the WWNs the tag applies to. Use 'untag' to remove tags. NOTE: tags are only supported in kernel 3.8 and above. ''' if wwn_or_tag == new_tag: return # Since all WWNs have a '.' in them, let's avoid confusion. if '.' in new_tag: raise ExecutionError("'.' not permitted in tag names.") src = list(self.find_tagged(wwn_or_tag)) if not src: raise ExecutionError("wwn_or_tag %s not found." % wwn_or_tag) old_tag_members = list(self.find_tagged(new_tag)) # handle overlap src_wwns = [na.node_wwn for na in src] old_tag_members = [old for old in old_tag_members if old.node_wwn not in src_wwns] for na in src: na.tag = new_tag # if joining a tag, take its config if old_tag_members: model = old_tag_members[0] for mlun in na.mapped_luns: mlun.delete() for mlun in model.mapped_luns: MappedLUN(na, mlun.mapped_lun, mlun.tpg_lun, mlun.write_protect) if self.parent.rtsnode.has_feature("auth"): for param in auth_params: setattr(na, "chap_" + param, getattr(model, "chap_" + param)) for item in model.list_attributes(writable=True): na.set_attribute(item, model.get_attribute(item)) for item in model.list_parameters(writable=True): na.set_parameter(item, model.get_parameter(item)) self.refresh() def ui_command_untag(self, wwn_or_tag): ''' Untag a NodeACL. Usage: untag Remove the tag given to one or more initiator WWNs. They will return to being displayed by WWN in the configuration tree, and will maintain settings from when they were tagged. ''' for na in list(self.find_tagged(wwn_or_tag)): na.tag = None self.refresh() def ui_complete_tag(self, parameters, text, current_param): ''' Parameter auto-completion method for user command tag @param parameters: Parameters on the command line. @type parameters: dict @param text: Current text of parameter being typed by the user. @type text: str @param current_param: Name of parameter to complete. @type current_param: str @return: Possible completions @rtype: list of str ''' if current_param == 'wwn_or_tag': completions = [n for n in self.all_names() if n.startswith(text)] else: completions = [] if len(completions) == 1: return [completions[0] + ' '] else: return completions ui_complete_untag = ui_complete_tag class UINodeACL(UIRTSLibNode): ''' A generic UI for a node ACL. Handles grouping multiple NodeACLs in UI via tags. All gets are performed against first NodeACL. All sets are performed on all NodeACLs. This is to make management of multiple ACLs easier. ''' ui_desc_attributes = { 'dataout_timeout': ('number', 'Data-Out timeout in seconds before invoking recovery.'), 'dataout_timeout_retries': ('number', 'Number of Data-Out timeout recovery attempts before failing a path.'), 'default_erl': ('number', 'Default Error Recovery Level.'), 'nopin_response_timeout': ('number', 'Nop-In response timeout in seconds.'), 'nopin_timeout': ('number', 'Nop-In timeout in seconds.'), 'random_datain_pdu_offsets': ('number', 'If set to 1, request random Data-In PDU offsets.'), 'random_datain_seq_offsets': ('number', 'If set to 1, request random Data-In sequence offsets.'), 'random_r2t_offsets': ('number', 'If set to 1, request random R2T (Ready To Transfer) offsets.'), } ui_desc_parameters = UITPG.ui_desc_parameters def __init__(self, name, parent): # Don't want to duplicate work in UIRTSLibNode, so call it but # del self.rtsnode to make sure we always use self.rtsnodes. self.rtsnodes = list(parent.find_tagged(name)) super(UINodeACL, self).__init__(name, self.rtsnodes[0], parent) del self.rtsnode if self.parent.parent.rtsnode.has_feature('auth'): for parameter in auth_params: self.define_config_group_param('auth', parameter, 'string') self.refresh() def ui_getgroup_auth(self, auth_attr): ''' This is the backend method for getting auths attributes. @param auth_attr: The auth attribute to get the value of. @type auth_attr: str @return: The auth attribute's value @rtype: str ''' # All should return same, so just return from the first one return getattr(self.rtsnodes[0], "chap_" + auth_attr) def ui_setgroup_auth(self, auth_attr, value): ''' This is the backend method for setting auths attributes. @param auth_attr: The auth attribute to set the value of. @type auth_attr: str @param value: The auth's value @type value: str ''' self.assert_root() if value is None: value = '' for na in self.rtsnodes: setattr(na, "chap_" + auth_attr, value) def refresh(self): self._children = set([]) for mlun in self.rtsnodes[0].mapped_luns: UIMappedLUN(mlun, self) def summary(self): msg = [] if self.name != self.rtsnodes[0].node_wwn: if len(self.rtsnodes) > 1: msg.append("(group of %d)" % len(self.rtsnodes)) else: msg.append("(%s)" % self.rtsnodes[0].node_wwn) status = None na = self.rtsnodes[0] tpg = self.parent.parent.rtsnode if tpg.has_feature("auth") and \ int(tpg.get_attribute("authentication")): if int(tpg.get_attribute("generate_node_acls")): msg.append("auth via tpg") else: status = True if not (na.chap_password and na.chap_userid): status = False if na.authenticate_target: msg.append("mutual auth") else: msg.append("1-way auth") msg.append("Mapped LUNs: %d" % len(self._children)) return (", ".join(msg), status) def ui_command_create(self, mapped_lun, tpg_lun_or_backstore, write_protect=None): ''' Creates a mapping to one of the TPG LUNs for the initiator referenced by the ACL. The provided I{tpg_lun_or_backstore} will appear to that initiator as LUN I{mapped_lun}. If the I{write_protect} flag is set to B{1}, the initiator will not have write access to the Mapped LUN. A storage object may also be given for the I{tpg_lun_or_backstore} parameter, in which case the TPG LUN will be created for that backstore before mapping the LUN to the initiator. If a TPG LUN for the backstore already exists, the Mapped LUN will map to that TPG LUN. Finally, a path to an existing block device or file can be given. If so, a storage object of the appropriate type is created with default parameters, followed by the TPG LUN and the Mapped LUN. SEE ALSO ======== B{delete} ''' self.assert_root() try: mapped_lun = int(mapped_lun) except ValueError: raise ExecutionError("mapped_lun must be an integer") try: if tpg_lun_or_backstore.startswith("lun"): tpg_lun_or_backstore = tpg_lun_or_backstore[3:] tpg_lun = int(tpg_lun_or_backstore) except ValueError: try: so = self.get_node(tpg_lun_or_backstore).rtsnode except ValueError: try: so = StorageObjectFactory(tpg_lun_or_backstore) self.shell.log.info("Created storage object %s." % so.name) except RTSLibError: raise ExecutionError("LUN, storage object, or path not valid") self.get_node("/backstores").refresh() ui_tpg = self.parent.parent for lun in ui_tpg.rtsnode.luns: if so == lun.storage_object: tpg_lun = lun.lun break else: lun_object = LUN(ui_tpg.rtsnode, storage_object=so) self.shell.log.info("Created LUN %s." % lun_object.lun) ui_lun = UILUN(lun_object, ui_tpg.get_node("luns")) tpg_lun = ui_lun.rtsnode.lun if tpg_lun in (ml.tpg_lun.lun for ml in self.rtsnodes[0].mapped_luns): self.shell.log.warning( "Warning: TPG LUN %d already mapped to this NodeACL" % tpg_lun) for na in self.rtsnodes: mlun = MappedLUN(na, mapped_lun, tpg_lun, write_protect) ui_mlun = UIMappedLUN(mlun, self) self.shell.log.info("Created Mapped LUN %s." % mlun.mapped_lun) return self.new_node(ui_mlun) def ui_complete_create(self, parameters, text, current_param): ''' Parameter auto-completion method for user command create. @param parameters: Parameters on the command line. @type parameters: dict @param text: Current text of parameter being typed by the user. @type text: str @param current_param: Name of parameter to complete. @type current_param: str @return: Possible completions @rtype: list of str ''' if current_param == 'tpg_lun_or_backstore': completions = [] for backstore in self.get_node('/backstores').children: for storage_object in backstore.children: completions.append(storage_object.path) completions.extend(lun.name for lun in self.parent.parent.get_node("luns").children) completions.extend(complete_path(text, lambda x: stat.S_ISREG(x) or stat.S_ISBLK(x))) completions = [c for c in completions if c.startswith(text)] else: completions = [] if len(completions) == 1: return [completions[0] + ' '] else: return completions def ui_command_delete(self, mapped_lun): ''' Deletes the specified I{mapped_lun}. SEE ALSO ======== B{create} ''' self.assert_root() for na in self.rtsnodes: mlun = MappedLUN(na, mapped_lun) mlun.delete() self.shell.log.info("Deleted Mapped LUN %s." % mapped_lun) self.refresh() def ui_complete_delete(self, parameters, text, current_param): ''' Parameter auto-completion method for user command delete. @param parameters: Parameters on the command line. @type parameters: dict @param text: Current text of parameter being typed by the user. @type text: str @param current_param: Name of parameter to complete. @type current_param: str @return: Possible completions @rtype: list of str ''' if current_param == 'mapped_lun': mluns = [str(mlun.mapped_lun) for mlun in self.rtsnodes[0].mapped_luns] completions = [mlun for mlun in mluns if mlun.startswith(text)] else: completions = [] if len(completions) == 1: return [completions[0] + ' '] else: return completions # Override these four methods to handle multiple NodeACLs def ui_getgroup_attribute(self, attribute): return self.rtsnodes[0].get_attribute(attribute) def ui_setgroup_attribute(self, attribute, value): self.assert_root() for na in self.rtsnodes: na.set_attribute(attribute, value) def ui_getgroup_parameter(self, parameter): return self.rtsnodes[0].get_parameter(parameter) def ui_setgroup_parameter(self, parameter, value): self.assert_root() for na in self.rtsnodes: na.set_parameter(parameter, value) def ui_command_info(self): ''' Since we don't have a self.rtsnode we can't use the base implementation of this method. We also want to not print node_wwn, but list *all* wwns for this entry. ''' info = self.rtsnodes[0].dump() for item in ('attributes', 'parameters', "node_wwn"): if item in info: del info[item] for name, value in sorted(six.iteritems(info)): if not isinstance (value, (dict, list)): self.shell.log.info("%s: %s" % (name, value)) self.shell.log.info("wwns:") for na in self.parent.find_tagged(self.name): self.shell.log.info(na.node_wwn) class UIMappedLUN(UIRTSLibNode): ''' A generic UI for MappedLUN objects. ''' def __init__(self, mapped_lun, parent): name = "mapped_lun%d" % mapped_lun.mapped_lun super(UIMappedLUN, self).__init__(name, mapped_lun, parent) self.refresh() def summary(self): mapped_lun = self.rtsnode is_healthy = True try: tpg_lun = mapped_lun.tpg_lun except RTSLibBrokenLink: description = "BROKEN LUN LINK" is_healthy = False else: if mapped_lun.write_protect: access_mode = 'ro' else: access_mode = 'rw' description = "lun%d %s/%s (%s)" \ % (tpg_lun.lun, tpg_lun.storage_object.plugin, tpg_lun.storage_object.name, access_mode) return (description, is_healthy) class UILUNs(UINode): ''' A generic UI for TPG LUNs. ''' def __init__(self, tpg, parent): super(UILUNs, self).__init__("luns", parent) self.tpg = tpg self.refresh() def refresh(self): self._children = set([]) for lun in self.tpg.luns: UILUN(lun, self) def summary(self): return ("LUNs: %d" % len(self._children), None) def ui_command_create(self, storage_object, lun=None, add_mapped_luns=None): ''' Creates a new LUN in the Target Portal Group, attached to a storage object. If the I{lun} parameter is omitted, the first available LUN in the TPG will be used. If present, it must be a number greater than 0. Alternatively, the syntax I{lunX} where I{X} is a positive number is also accepted. The I{storage_object} may be the path of an existing storage object, i.e. B{/backstore/pscsi0/mydisk} to reference the B{mydisk} storage object of the virtual HBA B{pscsi0}. It also may be the path to an existing block device or image file, in which case a storage object will be created for it first, with default parameters. If I{add_mapped_luns} is omitted, the global parameter B{auto_add_mapped_luns} will be used, else B{true} or B{false} are accepted. If B{true}, then after creating the LUN, mapped LUNs will be automatically created for all existing node ACLs, mapping the new LUN. SEE ALSO ======== B{delete} ''' self.assert_root() add_mapped_luns = \ self.ui_eval_param(add_mapped_luns, 'bool', self.shell.prefs['auto_add_mapped_luns']) try: so = self.get_node(storage_object).rtsnode except ValueError: try: so = StorageObjectFactory(storage_object) self.shell.log.info("Created storage object %s." % so.name) except RTSLibError: raise ExecutionError("storage object or path not valid") self.get_node("/backstores").refresh() if so in (l.storage_object for l in self.parent.rtsnode.luns): raise ExecutionError("lun for storage object %s/%s already exists" \ % (so.plugin, so.name)) if lun and lun.lower().startswith('lun'): lun = lun[3:] lun_object = LUN(self.tpg, lun, so) self.shell.log.info("Created LUN %s." % lun_object.lun) ui_lun = UILUN(lun_object, self) if add_mapped_luns: for acl in self.tpg.node_acls: if lun: mapped_lun = lun else: mapped_lun = 0 existing_mluns = [mlun.mapped_lun for mlun in acl.mapped_luns] if mapped_lun in existing_mluns: mapped_lun = None for possible_mlun in six.moves.range(LUN.MAX_LUN): if possible_mlun not in existing_mluns: mapped_lun = possible_mlun break if mapped_lun == None: self.shell.log.warning( "Cannot map new lun %s into ACL %s" % (lun_object.lun, acl.node_wwn)) else: mlun = MappedLUN(acl, mapped_lun, lun_object, write_protect=False) self.shell.log.info("Created LUN %d->%d mapping in node ACL %s" % (mlun.tpg_lun.lun, mlun.mapped_lun, acl.node_wwn)) self.parent.refresh() return self.new_node(ui_lun) def ui_complete_create(self, parameters, text, current_param): ''' Parameter auto-completion method for user command create. @param parameters: Parameters on the command line. @type parameters: dict @param text: Current text of parameter being typed by the user. @type text: str @param current_param: Name of parameter to complete. @type current_param: str @return: Possible completions @rtype: list of str ''' if current_param == 'storage_object': storage_objects = [] for backstore in self.get_node('/backstores').children: for storage_object in backstore.children: storage_objects.append(storage_object.path) completions = [so for so in storage_objects if so.startswith(text)] completions.extend(complete_path(text, lambda x: stat.S_ISREG(x) or stat.S_ISBLK(x))) else: completions = [] if len(completions) == 1: return [completions[0] + ' '] else: return completions def ui_command_delete(self, lun): ''' Deletes the supplied LUN from the Target Portal Group. The I{lun} must be a positive number matching an existing LUN. Alternatively, the syntax I{lunX} where I{X} is a positive number is also accepted. SEE ALSO ======== B{create} ''' self.assert_root() if lun.lower().startswith("lun"): lun = lun[3:] try: lun_object = LUN(self.tpg, lun) except: raise RTSLibError("Invalid LUN") lun_object.delete() self.shell.log.info("Deleted LUN %s." % lun) # Refresh the TPG as we need to also refresh acls MappedLUNs self.parent.refresh() def ui_complete_delete(self, parameters, text, current_param): ''' Parameter auto-completion method for user command delete. @param parameters: Parameters on the command line. @type parameters: dict @param text: Current text of parameter being typed by the user. @type text: str @param current_param: Name of parameter to complete. @type current_param: str @return: Possible completions @rtype: list of str ''' if current_param == 'lun': luns = [str(lun.lun) for lun in self.tpg.luns] completions = [lun for lun in luns if lun.startswith(text)] else: completions = [] if len(completions) == 1: return [completions[0] + ' '] else: return completions class UILUN(UIRTSLibNode): ''' A generic UI for LUN objects. ''' def __init__(self, lun, parent): name = "lun%d" % lun.lun super(UILUN, self).__init__(name, lun, parent) self.refresh() def summary(self): lun = self.rtsnode is_healthy = True try: storage_object = lun.storage_object except RTSLibBrokenLink: description = "BROKEN STORAGE LINK" is_healthy = False else: description = "%s/%s" % (storage_object.plugin, storage_object.name,) if storage_object.udev_path: description += " (%s)" % storage_object.udev_path return (description, is_healthy) class UIPortals(UINode): ''' A generic UI for TPG network portals. ''' def __init__(self, tpg, parent): super(UIPortals, self).__init__("portals", parent) self.tpg = tpg self.refresh() def refresh(self): self._children = set([]) for portal in self.tpg.network_portals: UIPortal(portal, self) def summary(self): return ("Portals: %d" % len(self._children), None) def _canonicalize_ip(self, ip_address): """ rtslib expects ipv4 addresses as a dotted-quad string, and IPv6 addresses surrounded by brackets. """ # Contains a '.'? Must be ipv4, right? if "." in ip_address: return ip_address return "[" + ip_address + "]" def ui_command_create(self, ip_address=None, ip_port=None): ''' Creates a Network Portal with specified I{ip_address} and I{ip_port}. If I{ip_port} is omitted, the default port for the target fabric will be used. If I{ip_address} is omitted, INADDR_ANY (0.0.0.0) will be used. Choosing IN6ADDR_ANY (::0) will listen on all IPv6 interfaces as well as IPv4, assuming IPV6_V6ONLY sockopt has not been set. Note: Portals on Link-local IPv6 addresses are currently not supported. SEE ALSO ======== B{delete} ''' self.assert_root() # FIXME: Add a specfile parameter to determine default port ip_port = self.ui_eval_param(ip_port, 'number', 3260) ip_address = self.ui_eval_param(ip_address, 'string', "0.0.0.0") if ip_port == 3260: self.shell.log.info("Using default IP port %d" % ip_port) if ip_address == "0.0.0.0": self.shell.log.info("Binding to INADDR_ANY (0.0.0.0)") portal = NetworkPortal(self.tpg, self._canonicalize_ip(ip_address), ip_port, mode='create') self.shell.log.info("Created network portal %s:%d." % (ip_address, ip_port)) ui_portal = UIPortal(portal, self) return self.new_node(ui_portal) def ui_complete_create(self, parameters, text, current_param): ''' Parameter auto-completion method for user command create. @param parameters: Parameters on the command line. @type parameters: dict @param text: Current text of parameter being typed by the user. @type text: str @param current_param: Name of parameter to complete. @type current_param: str @return: Possible completions @rtype: list of str ''' def list_eth_ips(): if not ethtool: return [] devcfgs = ethtool.get_interfaces_info(ethtool.get_devices()) addrs = set() for d in devcfgs: if d.ipv4_address: addrs.add(d.ipv4_address) addrs.add("0.0.0.0") for ip6 in d.get_ipv6_addresses(): addrs.add(ip6.address) addrs.add("::0") # only list ::0 if ipv6 present return sorted(addrs) if current_param == 'ip_address': completions = [addr for addr in list_eth_ips() if addr.startswith(text)] else: completions = [] if len(completions) == 1: return [completions[0] + ' '] else: return completions def ui_command_delete(self, ip_address, ip_port): ''' Deletes the Network Portal with specified I{ip_address} and I{ip_port}. SEE ALSO ======== B{create} ''' self.assert_root() portal = NetworkPortal(self.tpg, self._canonicalize_ip(ip_address), ip_port, mode='lookup') portal.delete() self.shell.log.info("Deleted network portal %s:%s" % (ip_address, ip_port)) self.refresh() def ui_complete_delete(self, parameters, text, current_param): ''' Parameter auto-completion method for user command delete. @param parameters: Parameters on the command line. @type parameters: dict @param text: Current text of parameter being typed by the user. @type text: str @param current_param: Name of parameter to complete. @type current_param: str @return: Possible completions @rtype: list of str ''' completions = [] # TODO: Check if a dict comprehension is acceptable here with supported # XXX: python versions. portals = {} all_ports = set([]) for portal in self.tpg.network_portals: all_ports.add(str(portal.port)) portal_ip = portal.ip_address.strip('[]') if not portal_ip in portals: portals[portal_ip] = [] portals[portal_ip].append(str(portal.port)) if current_param == 'ip_address': completions = [addr for addr in portals if addr.startswith(text)] if 'ip_port' in parameters: port = parameters['ip_port'] completions = [addr for addr in completions if port in portals[addr]] elif current_param == 'ip_port': if 'ip_address' in parameters: addr = parameters['ip_address'] if addr in portals: completions = [port for port in portals[addr] if port.startswith(text)] else: completions = [port for port in all_ports if port.startswith(text)] if len(completions) == 1: return [completions[0] + ' '] else: return completions class UIPortal(UIRTSLibNode): ''' A generic UI for a network portal. ''' def __init__(self, portal, parent): name = "%s:%s" % (portal.ip_address, portal.port) super(UIPortal, self).__init__(name, portal, parent) self.refresh() def summary(self): if self.rtsnode.iser: return('iser', True) return ('', True) def ui_command_enable_iser(self, boolean): ''' Enables or disables iSER for this NetworkPortal. If iSER is not supported by the kernel, this command will do nothing. ''' boolean = self.ui_eval_param(boolean, 'bool', False) self.rtsnode.iser = boolean self.shell.log.info("iSER enable now: %s" % self.rtsnode.iser) targetcli-fb-2.1.fb43/targetcli/version.py000066400000000000000000000011661270157724400205060ustar00rootroot00000000000000''' This file is part of targetcli. Copyright (c) 2011-2013 by Datera, Inc Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ''' __version__ = '2.1.fb43'