Sign Up
Log In
Log In
or
Sign Up
Places
All Projects
Status Monitor
Collapse sidebar
SUSE:SLE-12-SP1:GA
targetcli.1545
targetcli-git-update.patch
Overview
Repositories
Revisions
Requests
Users
Attributes
Meta
File targetcli-git-update.patch of Package targetcli.1545
diff --git a/.gitignore b/.gitignore index e9ba267..5a53423 100644 --- a/.gitignore +++ b/.gitignore @@ -12,7 +12,6 @@ debian/rtsadmin.substvars debian/rtsadmin/ debian/tmp/ dist/ -doc/ *.pyc *.swp dpkg-buildpackage.log @@ -21,3 +20,4 @@ dpkg-buildpackage.version redhat/*.spec ./rtsadmin-* log/ +\#*# diff --git a/Makefile b/Makefile index 7055bf1..4c41097 100644 --- a/Makefile +++ b/Makefile @@ -1,11 +1,29 @@ +# This file is part of TargetCLI. +# Copyright (c) 2011-2014 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. +# + NAME = targetcli GIT_BRANCH = $$(git branch | grep \* | tr -d \*) -VERSION = $$(basename $$(git describe --tags | tr - .)) +VERSION = $$(basename $$(git describe --tags | tr - . | grep -o '[0-9].*$$')) all: @echo "Usage:" @echo @echo " make deb - Builds debian packages." + @echo " make debinstall - Builds and installs debian packages." + @echo " (requires sudo access)" @echo " make rpm - Builds rpm packages." @echo " make release - Generates the release tarball." @echo @@ -14,7 +32,6 @@ all: 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 @@ -31,6 +48,8 @@ clean: @rm -fvr debian/${NAME}-frozen/ debian/${NAME}-python2.5/ @rm -fvr debian/${NAME}-python2.6/ debian/${NAME}/ debian/${NAME}-doc/ @rm -frv log/ + @find . -name *~ -exec rm -v {} \; + @find . -name \#*\# -exec rm -v {} \; @echo "Finished cleanup." cleanall: clean @@ -58,7 +77,7 @@ build/release-stamp: rmdir rpm @echo "Generating rpm changelog..." @( \ - version=$$(basename $$(git describe HEAD --tags | tr - .)); \ + version=$$(basename $$(git describe HEAD --tags | tr - . | grep -o '[0-9].*$$')); \ author=$$(git show HEAD --format="format:%an <%ae>" -s); \ date=$$(git show HEAD --format="format:%ad" -s \ | awk '{print $$1,$$2,$$3,$$5}'); \ @@ -68,7 +87,7 @@ build/release-stamp: ) >> $$(ls build/${NAME}-${VERSION}/*.spec) @echo "Generating debian changelog..." @( \ - version=$$(basename $$(git describe HEAD --tags | tr - .)); \ + version=$$(basename $$(git describe HEAD --tags | tr - . | grep -o '[0-9].*$$')); \ 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 \ @@ -107,6 +126,10 @@ build/deb-stamp: @for pkg in $$(ls dist/*_${VERSION}_*.deb); do echo " $${pkg}"; done @touch build/deb-stamp +debinstall: deb + @echo "Installing $$(ls dist/*_${VERSION}_*.deb)" + @sudo dpkg -i $$(ls dist/*_${VERSION}_*.deb) + rpm: release build/rpm-stamp build/rpm-stamp: @echo "Building rpm packages..." diff --git a/README b/README deleted file mode 100644 index 4923633..0000000 --- a/README +++ /dev/null @@ -1,10 +0,0 @@ -targetcli is an administration tool for managing RisingTide -Systems storage targets using the kernel LIO core target and compatible target -fabric modules. The targetcli CLI is built on top of the python configshell CLI -framework. - -The latest version of this program might be obtained at: -http://www.risingtidesystems.com/git/ - -To run the CLI from this directory use: -sudo PYTHONPATH=. ./scripts/targetcli diff --git a/README.md b/README.md new file mode 100644 index 0000000..1e1d21c --- /dev/null +++ b/README.md @@ -0,0 +1,74 @@ +# TargetCLI + +TargetCLI is an administration tool for managing the LIO Linux SCSI Target, +and its third-party target fabric modules and backend storage objects. + +Based on RTSLib, it allows direct manipulation of all SCSI Target objects like +storage objects, SCSI targets, TPGs, LUNs and ACLs, as well as manage startup +system configuration for the SCSI Target subsystem. + +TargetCLI can be used either as a regular CLI tool, one command at a time, or +as an interactive shell based on the python configshell CLI framework, with +full auto-complete support and inline documentation. + +TargetCLI is part of the Linux Kernel's SCSI Target's userspace management +tools. + +## Installation + +TargetCLI is currently part of several Linux distributions. In most cases, +simply installing the version packaged by your favorite Linux distribution is +the best way to get it running. + +## Building from source + +The packages are very easy to build and install from source as long as +you're familiar with your Linux Distribution's package manager: + +1. Clone the github repository for TargetCLI using `git clone + https://github.com/Datera/targetcli.git`. + +2. Make sure build dependencies are installed. To build TargetCLI, you will need: + + * GNU Make. + * python 2.6 or 2.7 + * A few python libraries: rtslib, configshell, lio-utils + * Your favorite distribution's package developement tools, like rpm for + Redhat-based systems or dpkg-dev and debhelper for Debian systems. + +3. From the cloned git repository, run `make deb` to generate a Debian + package, or `make rpm` for a Redhat package. + +4. The newly built packages will be generated in the `dist/` directory. + +5. To cleanup the repository, use `make clean` or `make cleanall` which also + removes `dist/*` files. + +## Documentation + +A manpage is provided with this packages, simply use `man targetcli` to get +more information. + +An other good source of information is the http://linux-iscsi.org wiki, +offering many resources such as a the TargetCLI User's Guide, online at +http://linux-iscsi.org/wiki/targetcli. + +## Mailing-list + +All contributions, suggestions and bugfixes are welcome! + +To report a bug, submit a patch or simply stay up-to-date on the Linux SCSI +Target developments, you can subscribe to the Linux Kernel SCSI Target +development mailing-list by sending an email message containing only +`subscribe target-devel` to <mailto:majordomo@vger.kernel.org> + +The archives of this mailing-list can be found online at +http://dir.gmane.org/gmane.linux.scsi.target.devel + +## Author + +TargetCLI was developed by Datera, Inc. +http://www.datera.io + +The original author and current maintainer is +Jerome Martin <jxm@netiant.com> diff --git a/debian/README.Debian b/debian/README.Debian deleted file mode 100644 index 78a1f14..0000000 --- a/debian/README.Debian +++ /dev/null @@ -1,13 +0,0 @@ -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. diff --git a/debian/control b/debian/control index af078bc..1dfd3ed 100644 --- a/debian/control +++ b/debian/control @@ -1,15 +1,16 @@ Source: targetcli -Section: python +Section: net Priority: optional -Maintainer: Jerome Martin <jxm@risingtidesystems.com> -Build-Depends: debhelper(>= 5.0.1), python2.6, build-essential, python-dev, python2.6-dev, python-epydoc, zlib1g-dev, python-configshell, python-rtslib -Standards-Version: 3.8.1 +Standards-Version: 3.9.2 +Homepage: https://github.com/Datera/targetcli +Maintainer: Jerome Martin <jxm@netiant.com> +Build-Depends: debhelper(>= 7.0.50~), python(>= 2.6.6-3~), python-rtslib, python-configshell Package: targetcli Architecture: all -Depends: python (>= 2.6)|python2.6, python-configshell, python-rtslib, lio-utils -Suggests: targetcli-doc -Conflicts: targetcli-frozen -Description: CLI shell for the RisingTide Systems target. +Depends: ${python:Depends}, ${misc:Depends}, python-configshell, python-rtslib, python-prettytable, lsb-base(>=3.2-14) +Provides: ${python:Provides} +Conflicts: targetcli-frozen, rtsadmin-frozen, rtsadmin, lio-utils +Description: CLI and interactive shell to manage Linux SCSI Targets . - This package contains the targetcli CLI. + Part of the Linux Kernel SCSI Target's userspace management tools diff --git a/debian/copyright b/debian/copyright index 253cf82..8ac5d63 100644 --- a/debian/copyright +++ b/debian/copyright @@ -1,11 +1,4 @@ -This package was originally debianized by Jerome Martin <jxm@risingtidesystems.com> -on Thu Nov 19 12:00:01 UTC 2009. It is currently maintained by Jerome Martin -<jxm@risingtidesystems.com>. - -Upstream Author: Jerome Martin <jxm@risingtidesystems.com> - -This file is part of ConfigShell. -Copyright (c) 2011-2013 by Datera, Inc +Copyright (c) 2011-2014 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 diff --git a/debian/pyversions b/debian/pyversions deleted file mode 100644 index 0c043f1..0000000 --- a/debian/pyversions +++ /dev/null @@ -1 +0,0 @@ -2.6- diff --git a/debian/rules b/debian/rules index 664cd72..77d65a2 100755 --- a/debian/rules +++ b/debian/rules @@ -1,56 +1,21 @@ #!/usr/bin/make -f build_dir = build -install_dir = debian/tmp -python = $(shell \ - python=$$(which python2.6 2>/dev/null); \ - if [ -z $$python ]; then python=$$(which python 2>/dev/null); fi; \ - if [ -z $$python ]; then python="/usr/bin/python"; fi; \ - echo $$python) -setup = $(python) ./setup.py --quiet +install_dir = debian/targetcli -binary: binary-indep +%: + dh $@ --with python2 -binary-arch: +override_dh_auto_clean: + # manually clean any *.pyc files + rm -rf targetcli/*.pyc -binary-indep: build install - dh_testdir - dh_testroot - dh_installchangelogs - dh_installdocs - dh_installman - dh_install --list-missing --sourcedir $(install_dir) - dh_fixperms - dh_compress -X.py - dh_installdeb - dh_gencontrol - dh_md5sums - dh_builddeb +override_dh_auto_build: + python setup.py build --build-base $(build_dir) -install: build - dh_testdir - dh_testroot - dh_installdirs - -build: build-stamp -build-stamp: - dh_testdir - # Build the source package - $(setup) build --build-base $(build_dir) install --no-compile --install-purelib $(install_dir)/lib/targetcli --install-scripts $(install_dir)/bin - echo "2.6" > $(install_dir)/lib/targetcli/.version - # Cleanup - rm -f $(install_dir)/lib/targetcli/targetcli/*.pyc - touch build-stamp - -clean: - dh_testdir - dh_testroot - rm -f build-stamp - $(setup) clean - find . -name "*.pyc" | xargs rm -f - find . -name "*.pyo" | xargs rm -f - rm -rf $(build_dir) $(install_dir) - dh_clean - -.PHONY: binary binary-indep install build clean +override_dh_auto_install: + python setup.py install --prefix=/usr --no-compile \ + --install-layout=deb --root=$(CURDIR)/$(install_dir) +override_dh_installinit: + dh_installinit --name target diff --git a/debian/target.init b/debian/target.init new file mode 120000 index 0000000..13c733b --- /dev/null +++ b/debian/target.init @@ -0,0 +1 @@ +../scripts/target.init \ No newline at end of file diff --git a/debian/targetcli-doc.docs b/debian/targetcli-doc.docs deleted file mode 100644 index 2e1165c..0000000 --- a/debian/targetcli-doc.docs +++ /dev/null @@ -1,6 +0,0 @@ -doc/README -doc/COPYING -doc/targetcli_reference.html -doc/targetcli_reference.pdf -doc/targetcli_reference_for_print.pdf -doc/rtslogo.png diff --git a/debian/targetcli.dirs b/debian/targetcli.dirs index f430183..a808d2b 100644 --- a/debian/targetcli.dirs +++ b/debian/targetcli.dirs @@ -1 +1 @@ -usr/share/python-support/ +/etc/target/ \ No newline at end of file diff --git a/debian/targetcli.docs b/debian/targetcli.docs index 1f562b3..7758d58 100644 --- a/debian/targetcli.docs +++ b/debian/targetcli.docs @@ -1,2 +1,2 @@ -README +README.md COPYING diff --git a/debian/targetcli.install b/debian/targetcli.install deleted file mode 100644 index 5a1dd4b..0000000 --- a/debian/targetcli.install +++ /dev/null @@ -1,2 +0,0 @@ -lib/targetcli usr/share/python-support -bin /usr diff --git a/debian/targetcli.manpages b/debian/targetcli.manpages new file mode 100644 index 0000000..60051b6 --- /dev/null +++ b/debian/targetcli.manpages @@ -0,0 +1 @@ +doc/targetcli.8 diff --git a/debian/targetcli.postinst b/debian/targetcli.postinst deleted file mode 100755 index 8412fc6..0000000 --- a/debian/targetcli.postinst +++ /dev/null @@ -1,17 +0,0 @@ -#!/bin/sh -for lib in lib lib64; do - for python in python2.6; do - if [ -e /usr/${lib}/${python} ]; then - if [ ! -e /usr/${lib}/${python}/targetcli ]; then - mkdir /usr/${lib}/${python}/targetcli - for source in /usr/share/python-support/targetcli/targetcli/*.py; do - ln -sf ${source} /usr/${lib}/${python}/targetcli/ - done - python_path=$(which ${python} 2>/dev/null) - if [ ! -z $python_path ]; then - ${python} -c "import compileall; compileall.compile_dir('/usr/${lib}/${python}/targetcli', force=1)" - fi - fi - fi - done -done diff --git a/debian/targetcli.preinst b/debian/targetcli.preinst deleted file mode 100755 index 58f5f2a..0000000 --- a/debian/targetcli.preinst +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/sh -rm -f /usr/share/python-support/targetcli/targetcli/*.pyc -rm -f /usr/share/python-support/targetcli/targetcli/*.pyo diff --git a/debian/targetcli.prerm b/debian/targetcli.prerm deleted file mode 100755 index 7603750..0000000 --- a/debian/targetcli.prerm +++ /dev/null @@ -1,8 +0,0 @@ -#!/bin/sh -for lib in lib lib64; do - for python in python2.6; do - if [ -e /usr/${lib}/${python}/targetcli ]; then - rm -rf /usr/${lib}/${python}/targetcli - fi - done -done diff --git a/doc/targetcli.8 b/doc/targetcli.8 new file mode 100644 index 0000000..68827a8 --- /dev/null +++ b/doc/targetcli.8 @@ -0,0 +1,232 @@ +.TH targetcli 8 +.SH NAME +.B targetcli +.SH DESCRIPTION +.B targetcli +is a shell for viewing, editing, and saving the configuration of +the kernel's target subsystem, also known as TCM/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 +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 <cmd>" +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 EXAMPLES +To export a storage resource, 1) define a storage object using +a backstore, then 2) export the object via a network fabric, such as +iSCSI or FCoE. +.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 +In addition to the +.I fileio +backstore for file-backed volumes, other backstore types include +.I iblock +for block-device-backed volumes, and +.I pscsi +for volumes backed by local SCSI devices. +.I rd_mcp +backstore creates ram-based storage objects. See the built-in help +for more details on the required parameters for each backstore type. +.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. +.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. Sane defaults are used if these are not +specified. +.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) 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.) +.P +.B acls/iqn.1994-05.com.redhat:4321576890 set authentication=0 +.br +Purely for example, make the LUNs in the ACL accessible without +authentication. See below for more information on configuring authentication. +.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. +.P +This command must be 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 exit +.br +Leave the configuration shell. +.SH SETTINGS GROUPS +Settings are broken into groups. Individual settings are accessed by +.B "get <group> <setting>" +and +.BR "set <group> <setting>=<value>" , +and the settings of an entire group may be displayed by +.BR "get <group>" . +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. Global settings +are saved to ~/.targetcli/ upon exit, unlike other groups. +.SS BACKSTORE-SPECIFIC +.B attribute +.br +/backstore/<type>/<name> 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/<target_iqn>/tpgX configuration node. ISCSI-specific parameters such as +.IR AuthMethod , +.IR MaxBurstLength , +.IR IFMarker , +.IR DataDigest , +and similar. +.P +.B attribute +.br +/iscsi/<target_iqn>/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/<target_iqn>/tpgX/acls/<initiator_iqn> configuration node. Set the +userid and password for full-feature phase for this ACL. +.SH FILES +.B /etc/target/* +.br +.B /var/lib/target/* +.SH AUTHOR +Written by Jerome Martin <jxm@risingtidesystems.com>. +.br +Man page written by Andy Grover <agrover@redhat.com>. +.SH REPORTING BUGS +Report bugs to <target-devel@vger.kernel.org> diff --git a/rpm/targetcli.spec.tmpl b/rpm/targetcli.spec.tmpl index 00fa6f6..782d07e 100644 --- a/rpm/targetcli.spec.tmpl +++ b/rpm/targetcli.spec.tmpl @@ -11,7 +11,8 @@ 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, lio-utils +Requires: python-rtslib, python-configshell, python-prettytable +Conflicts: targetcli-frozen, rtsadmin-frozen, rtsadmin, lio-utils Vendor: Datera, Inc. %description @@ -26,6 +27,9 @@ RisingTide Systems generic SCSI target CLI shell. %install rm -rf %{buildroot} %{__python} setup.py install --skip-build --root=%{buildroot} --prefix=usr +mkdir -p %_mandir/man8/ +mv doc/targetcli.8 %_mandir/man8/targetcli.8.gz +mkdir -p /etc/target %clean rm -rf %{buildroot} @@ -33,7 +37,9 @@ rm -rf %{buildroot} %files %defattr(-,root,root,-) %{python_sitelib} +/etc/target %{_bindir}/targetcli -%doc COPYING README +%{_bindir}/targetcli-ng +%doc COPYING README.md %_mandir/man8/targetcli.8.gz %changelog diff --git a/scripts/target.init b/scripts/target.init new file mode 100755 index 0000000..0e1fb4a --- /dev/null +++ b/scripts/target.init @@ -0,0 +1,241 @@ +#! /bin/sh +### BEGIN INIT INFO +# Provides: target +# Required-Start: $network $remote_fs $syslog +# Required-Stop: $network $remote_fs $syslog +# Default-Start: 2 3 4 5 +# Default-Stop: 0 1 6 +# Short-Description: The Linux SCSI Target service +### END INIT INFO + +# PATH should only include /usr/* if it runs after the mountnfs.sh script +PATH=/sbin:/usr/sbin:/bin:/usr/bin +DESC="The Linux SCSI Target" +NAME=target +DAEMON=/usr/bin/targetcli +DAEMON_ARGS="" +SCRIPTNAME=/etc/init.d/$NAME + +CFS_BASE="/sys/kernel/config/" +CFS_TGT="${CFS_BASE}/target" +CORE_MODS="target_core_mod target_core_pscsi target_core_iblock target_core_file" +STARTUP_CONFIG="/etc/target/scsi_target.lio" + +# Read configuration variable file if it is present +[ -r /etc/default/$NAME ] && . /etc/default/$NAME + +# Load the VERBOSE setting and other rcS variables +. /lib/init/vars.sh + +# Define LSB log_* functions - requires lsb-base (>= 3.2-14) +. /lib/lsb/init-functions + +load_specfiles() +{ +FABRIC_MODS=$(python 2>/dev/null << EOF +from rtslib import RTSRoot +print(" ".join(["%s:%s" % (fm.spec["kernel_module"], + fm.spec["configfs_group"]) + for fm in RTSRoot().fabric_modules])) +EOF +) +} + +check_install() +{ + # Check the system installation + INSTALL=ok + + python -c "from rtslib import Config" > /dev/null 2>&1 + if [ $? != 0 ]; then + log_failure_msg "Cannot load rtslib" + INSTALL=nok + fi + + if [ "${INSTALL}" != ok ]; then + exit 0 + else + log_action_msg "${DESC} looks properly installed" + fi +} + +load_configfs() +{ + modprobe configfs > /dev/null 2>&1 + if [ "$?" != 0 ]; then + log_failure_msg "Failed to load configfs kernel module" + return 1 + fi + mount -t configfs configfs ${CFS_BASE} > /dev/null 2>&1 + case "$?" in + 0) log_warning_msg "The configfs filesystem was not mounted, consider adding it to fstab";; + 32) log_action_msg "The configfs filesystem is already mounted";; + *) log_failure_msg "Failed to mount configfs"; return 1;; + esac +} + +load_modules() +{ + for MODULE in ${CORE_MODS}; do + modprobe "${MODULE}" > /dev/null 2>&1 + if [ "$?" != 0 ]; then + log_failure_msg "Failed to load target core module ${MODULE}" + return 1 + else + log_action_msg "Loaded ${MODULE} module" + fi + done + for MOD_SPEC in ${FABRIC_MODS}; do + MODULE="$(echo ${MOD_SPEC} | awk -F : '{print $1}')" + CFS_GROUP="${CFS_TGT}/$(echo ${MOD_SPEC} | awk -F : '{print $2}')" + modprobe "${MODULE}" > /dev/null 2>&1 + if [ "$?" != 0 ]; then + log_warning_msg "Failed to load fabric module ${MODULE}" + else + mkdir "${CFS_GROUP}" > /dev/null 2>&1 + if [ ! -d "${CFS_GROUP}" ]; then + log_warning_msg "Failed to create ${CFS_GROUP}" + else + log_action_msg "Loaded and enabled fabric module ${MODULE}" + fi + fi + done +} + +unload_modules() +{ + RETCODE=0 + for GROUP in ${CFS_GROUPS}; do + CFS_GROUP="${CFS_TGT}/${GROUP}" + done + + for MOD_SPEC in ${FABRIC_MODS}; do + MODULE="$(echo ${MOD_SPEC} | awk -F : '{print $1}')" + CFS_GROUP="${CFS_TGT}/$(echo ${MOD_SPEC} | awk -F : '{print $2}')" + if [ ! -z "$(lsmod | grep ^${MODULE}\ )" ]; then + rmdir "${CFS_GROUP}" > /dev/null 2>&1 + if [ -d "${CFS_GROUP}" ]; then + log_failure_msg "Failed to remove ${CFS_GROUP}" + RETCODE=1 + else + rmmod "${MODULE}" > /dev/null 2>&1 + if [ "$?" != 0 ]; then + log_failure_msg "Failed to unload fabric module ${MODULE}" + RETCODE=1 + else + log_action_msg "Unloaded ${MODULE} fabric module" + fi + fi + else + log_warning_msg "Fabric module ${MODULE} is not loaded" + fi + done + + MODULES="$(echo ${CORE_MODS} | tac -s ' ')" + for MODULE in ${MODULES}; do + if [ ! -z "$(lsmod | grep ^${MODULE}\ )" ]; then + rmmod "${MODULE}" > /dev/null 2>&1 + if [ "$?" != 0 ]; then + log_failure_msg "Failed to unload target core module ${MODULE}" + RETCODE=1 + else + log_action_msg "Unloaded ${MODULE} target core module" + fi + else + log_warning_msg "Target core module ${MODULE} is not loaded" + fi + done + + return "${RETCODE}" +} + +load_config() +{ +if [ -e "${STARTUP_CONFIG}" ]; then +export __STARTUP_CONFIG="${STARTUP_CONFIG}" +python 2> /dev/null << EOF +import os, rtslib +config = rtslib.Config() +config.load(os.environ['__STARTUP_CONFIG'], allow_new_attrs=True) +list(config.apply()) +EOF + if [ "$?" != 0 ]; then + unset __STARTUP_CONFIG + log_failure_msg "Failed to load ${STARTUP_CONFIG}" + return 1 + else + unset __STARTUP_CONFIG + log_action_msg "Loaded ${STARTUP_CONFIG}" + fi +else + log_warning_msg "No ${STARTUP_CONFIG} to load" +fi +} + +clear_config() +{ +python 2> /dev/null << EOF +from rtslib import Config +config = Config() +list(config.apply()) +EOF + +if [ "$?" != 0 ]; then + log_failure_msg "Failed to clear configuration" + return 1 +else + log_action_msg "Cleared configuration" +fi +} + +do_start() +{ + load_specfiles # Fill in FABRIC_MODS and CFS_GROUPS + check_install && load_configfs && load_modules && load_config + if [ "$?" != 0 ]; then + log_failure_msg "Could not start ${DESC}" + return 1 + else + log_success_msg "Started ${DESC}" + fi +} + +do_stop() +{ + load_specfiles # Fill in FABRIC_MODS and CFS_GROUPS + clear_config && unload_modules + if [ "$?" != 0 ]; then + log_failure_msg "Could not stop ${DESC}" + return 1 + else + log_success_msg "Stopped ${DESC}" + fi +} + +do_status() +{ + if [ -d ${CFS_TGT} ]; then + log_action_msg "${DESC} is started" + return 0 + else + log_action_msg "${DESC} is stopped" + return 1 + fi +} + +case "$1" in + start) + do_start + ;; + stop) + do_stop + ;; + status) + do_status ;; + restart|force-reload) + do_stop && do_start ;; + *) + echo "Usage: $SCRIPTNAME {start|stop|status|restart|force-reload}" >&2 + exit 3 + ;; +esac diff --git a/scripts/targetcli b/scripts/targetcli index 913bcbc..b3b4ff2 100755 --- a/scripts/targetcli +++ b/scripts/targetcli @@ -3,7 +3,7 @@ Starts the targetcli CLI shell. This file is part of targetcli. -Copyright (c) 2011-2013 by Datera, Inc +Copyright (c) 2011-2014 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 @@ -18,6 +18,7 @@ License for the specific language governing permissions and limitations under the License. ''' +import sys from os import getuid from targetcli import UIRoot from rtslib import RTSLibError @@ -56,10 +57,6 @@ def main(): is_root = False shell = TargetCLI('~/.targetcli') - shell.con.display("targetcli %s (rtslib %s)\n" - "Copyright (c) 2011-2013 by Datera, Inc.\n" - "All rights reserved." - % (targetcli_version, rtslib_version)) if not is_root: shell.con.display("You are not root, disabling privileged commands.\n") @@ -69,8 +66,17 @@ def main(): root_node.refresh() except RTSLibError, error: shell.con.display(shell.con.render_text(str(error), 'red')) - else: - shell.run_interactive() + + if len(sys.argv) > 1: + shell.run_cmdline(" ".join(sys.argv[1:])) + sys.exit(0) + + shell.con.display("targetcli %s (rtslib %s)\n" + "Copyright (c) 2011-2014 by Datera, Inc.\n" + "All rights reserved." + % (targetcli_version, rtslib_version)) + shell.con.display('') + shell.run_interactive() if __name__ == "__main__": main() diff --git a/scripts/targetcli-ng b/scripts/targetcli-ng new file mode 100755 index 0000000..bdc9ee7 --- /dev/null +++ b/scripts/targetcli-ng @@ -0,0 +1,59 @@ +#!/usr/bin/env python +''' +This file is part of the LIO SCSI Target. + +Copyright (c) 2012-2014 by Datera, Inc. +More information on www.datera.io. + +Original author: Jerome Martin <jxm@netiant.com> + +Datera and LIO are trademarks of Datera, Inc., which may be registered in some +jurisdictions. + +Licensed under the Apache License, Version 2.0 (the "License"); you may +not use this file except in compliance with the License. You may obtain +a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +License for the specific language governing permissions and limitations +under the License. +''' +import sys +from pyparsing import ParseException +from rtslib.config import ConfigError +from targetcli.cli_live import CliLive +from targetcli.cli_config import CliConfig +from targetcli.cli_logger import logger as log + +# TODO Add tests for non-interactive mode +# TODO Add batch mode if stdin is not a terminal + +if __name__ == '__main__': + try: + args = sys.argv[1:] + if not args: + config = 'live' + CliLive(interactive=True).cmdloop() + elif args[0] == "configure" and len(args) == 1: + config = 'candidate' + CliConfig(interactive=True).cmdloop() + elif args[0] == "configure": + config = 'candidate' + CliConfig(interactive=False).onecmd(" ".join(args[1:])) + else: + config = 'live' + CliLive(interactive=False).onecmd(" ".join(args)) + + except IOError, e: + log.critical("Failed to read %s configuration: %s" % (config, e)) + log.info("Check your user permissions") + + except ParseException, e: + log.critical("Failed to parse %s configuration: %s" % (config, e)) + + except ParseException, e: + log.critical("Failed to validate %s configuration: %s" % (config, e)) diff --git a/setup.py b/setup.py index 5746204..ab8e6e8 100755 --- a/setup.py +++ b/setup.py @@ -1,7 +1,7 @@ #! /usr/bin/env python ''' This file is part of targetcli. -Copyright (c) 2011-2013 by Datera, Inc +Copyright (c) 2011-2014 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 @@ -25,7 +25,7 @@ VERSION = str(PKG.__version__) (AUTHOR, EMAIL) = re.match('^(.*?)\s*<(.*)>$', PKG.__author__).groups() URL = PKG.__url__ LICENSE = PKG.__license__ -SCRIPTS = ["scripts/targetcli"] +SCRIPTS = ["scripts/targetcli", "scripts/targetcli-ng"] DESCRIPTION = PKG.__description__ setup(name=PKG.__name__, diff --git a/targetcli/__init__.py b/targetcli/__init__.py index f57f9c2..f797b47 100644 --- a/targetcli/__init__.py +++ b/targetcli/__init__.py @@ -1,6 +1,6 @@ ''' This file is part of targetcli. -Copyright (c) 2011-2013 by Datera, Inc +Copyright (c) 2011-2014 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 diff --git a/targetcli/cli.py b/targetcli/cli.py new file mode 100644 index 0000000..c4fb174 --- /dev/null +++ b/targetcli/cli.py @@ -0,0 +1,325 @@ +''' +This file is part of the LIO SCSI Target. + +Copyright (c) 2012-2014 by Datera, Inc. +More information on www.datera.io. + +Original author: Jerome Martin <jxm@netiant.com> + +Datera and LIO are trademarks of Datera, Inc., which may be registered in some +jurisdictions. + +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 pyparsing as pp +import sys, tty, cmd, termios, readline, traceback + +import rtslib.config, rtslib.config_tree +from targetcli.cli_logger import logger as log +from rtslib.config import ConfigError + +# TODO Implement | filters: top N, last N, page, grep +# TODO Redo help summary, using 2 columns: cmd, short description + +class CliError(Exception): + pass + +class Cli(cmd.Cmd): + ''' + Our base Cli class, common to both CliLive and CliConfig + ''' + intro = '' + log_levels = {'debug': 10, 'info': 20, 'warning': 30, + 'error': 40, 'critical': 50} + + def __init__(self, interactive, history_path): + ''' + Initializes a new Cli object. + + interactive is a boolean to run either interactively or in batch mode + history_path is the path to the command-line history file + ''' + cmd.Cmd.__init__(self) + self.debug_level = 'off' + self.last_traceback = None + self.interactive = interactive + self.do_save_history = self.interactive + if self.interactive: + self.load_history() + readline.set_completer_delims(' \t\n`~!@#$%^&*()=+[{]}\\|;\'",<>/?') + + def do_EOF(self, options): + sys.stdout.write("exit\n") + return self.do_exit(options) + + def _complete_options(self, text, line, begidx, endidx, options): + ''' + Helper to autocomplete one or more options out of options, without any + ordering considerations. + ''' + # TODO Add middle-of-line completion + prev_options = line.split()[1:] + if text: + prev_options = prev_options[:-1] + return ["%s " % name for name in options + if name.startswith(text) + if name.strip() not in prev_options] + + def _complete_one_option(self, text, line, begidx, endidx, options): + ''' + Helper to autocomplete a single option out of options. + ''' + # TODO Add middle-of-line completion + prev_options = line.split()[1:] + if text: + prev_options = prev_options[:-1] + return ["%s " % name for name in options + if name.startswith(text) + if not prev_options] + + def _complete_path(self, text, line, begidx, endidx, prefix=None): + ''' + Helper to autocomplete a configuration path. + ''' + # TODO Add middle-of-line completion + pattern = line.partition(' ')[2] + if prefix is None: + prefix = '' + + # Are we completing an attr/obj value/id or a group? + nodes_last_key = self.config.search(("%s %s.*" + % (prefix, pattern)).strip()) + # Or an attr/obj name/class ? + nodes_first_key = [node for node + in self.config.search(("%s %s.* .*" + % (prefix, pattern)).strip()) + if node.data['type'] != 'group'] + completions = [] + completions.extend(node.key[-1] for node in nodes_last_key) + completions.extend(node.key[0] for node in nodes_first_key) + return ["%s " % c for c in completions if c.startswith(text)] + + def _complete_filepath(self, text, line, begidx, endidx): + ''' + Helper to autocomplete file paths. + ''' + # TODO Implement this + return [] + + def save_history(self): + ''' + Saves the command history. + ''' + if not self.do_save_history: + return + try: + readline.write_history_file(self.history_path) + except Exception, e: + raise CliError("Failed to save command history, disabling: %s", e) + self.do_save_history = False + + def load_history(self): + ''' + Loads the command history. + ''' + try: + readline.read_history_file(self.history_path) + except IOError, e: + log.debug("Error while reading history: %s" % e) + + def clear_history(self): + ''' + Clears the command history. + ''' + readline.clear_history() + + def emptyline(self): + ''' + Just go on with a new prompt line if the user enters an empty line. + ''' + pass + + def cmdloop(self): + ''' + The main REPL loop. + ''' + intro = self.intro + while True: + try: + cmd.Cmd.cmdloop(self, intro=intro) + except KeyboardInterrupt: + sys.stdout.write("^C\n") + intro = '' + else: + break + + def onecmd(self, line): + ''' + Executes a command line. + ''' + try: + result = cmd.Cmd.onecmd(self, line) + except pp.ParseException, e: + log.error("Unknown syntax: %s at char %d" % (e.msg, e.loc)) + return None + except ConfigError, e: + self.last_traceback = traceback.format_exc() + log.error(str(e)) + except CliError, e: + self.last_traceback = traceback.format_exc() + log.error(str(e)) + except Exception, e: + self.last_traceback = traceback.format_exc() + log.error("%s: %s\n" % (e.__class__.__name__, e)) + return None + else: + self.save_history() + return result + + def completenames(self, text, *ignored): + return ["%s " % name[3:] for name in self.get_names() + if name.startswith("do_%s" % text) + if not name in ['do_EOF']] + + def getchar(self): + ''' + Returns the first character read from stdin, without waiting for the + user to hit enter. + ''' + fd = sys.stdin.fileno() + tcattr_backup = termios.tcgetattr(fd) + try: + tty.setraw(sys.stdin.fileno()) + char = sys.stdin.read(1) + finally: + termios.tcsetattr(fd, termios.TCSADRAIN, tcattr_backup) + return char + + def yes_no(self, question, default=None): + ''' + Asks a yes/no question to be answered by typing a single 'y' or 'n' + character. If we do not run in interactive mode, returns None. Else + returns True for yes and False for not. + + default can either be True (yes is the default), False (no is the + default) or None (no default). + ''' + keys = {'\x03': '^C', '\x04': '^D'} + if not self.interactive: + result = None + else: + if default is None: + choices = "y/n" + elif default is True: + choices = "Y/n" + dfl_key = 'y' + elif default is False: + choices = "y/N" + dfl_key = 'n' + key = None + replies = ['y', 'n', 'Y', 'N'] + if default is not None: + replies.append('\r') + while key not in replies: + log.debug("Got key %r" % key) + sys.stdout.write("%s [%s] " % (question, choices)) + key = self.getchar() + key = keys.get(key, key) + if key == '\r' and default is not None: + sys.stdout.write("%s\n" % dfl_key) + else: + sys.stdout.write("%s\n" % key) + if key in ['^C', '^D']: + raise CliError("Aborted") + if key == '\r': + result = default + elif key.lower() == 'y': + result = True + else: + result = False + + log.debug("yes_no(%s) -> %r" % (question, result)) + return result + + def parse(self, line, header, grammar): + ''' + Parses line using a pyparsing grammar. + Returns the parse tree as a list. + ''' + if not grammar: + grammar = pp.Empty() + grammar = pp.Literal(header) + grammar + line = "%s %s" % (header, line) + log.debug("Parsing line '%s'" % line) + tokens = grammar.parseString(line, parseAll=True).asList() + log.debug("Got parse tree %s" % tokens) + return tokens + + def do_trace(self, options): + ''' + trace + + Displays the last exception trace for the current mode. + + This is useful only for debugging the application. Your lio support + team might ask you to run this command to help understanding an issue + you're experimenting. + ''' + options = self.parse(options, 'trace', '')[1:] + if self.last_traceback is not None: + log.error(self.last_traceback) + else: + log.error("No previous exception traceback.") + + def do_debug(self, options): + ''' + debug [off|cli|api|all] + + Controls the debug messages level: + + off disables all debug message + cli enables only cli debug messages + api also enables Config API messages + all adds even more details to api debug + + With no option, displays the current debug level. + ''' + syntax = pp.Optional(pp.oneOf(["off", "cli", "api", "all"])) + options = self.parse(options, 'debug', syntax)[1:] + + if not options: + log.info("Current debug level: %s" % self.debug_level) + else: + self.debug_level = options[0] + if self.debug_level == 'off': + log.setLevel(self.log_levels['info']) + rtslib.config.log.setLevel(self.log_levels['info']) + rtslib.config_tree.log.setLevel(self.log_levels['info']) + elif self.debug_level == 'cli': + log.setLevel(self.log_levels['debug']) + rtslib.config.log.setLevel(self.log_levels['info']) + rtslib.config_tree.log.setLevel(self.log_levels['info']) + elif self.debug_level == 'api': + log.setLevel(self.log_levels['debug']) + rtslib.config.log.setLevel(self.log_levels['debug']) + rtslib.config_tree.log.setLevel(self.log_levels['info']) + elif self.debug_level == 'all': + log.setLevel(self.log_levels['debug']) + rtslib.config.log.setLevel(self.log_levels['debug']) + rtslib.config_tree.log.setLevel(self.log_levels['debug']) + + log.info("Debug level is now: %s" % self.debug_level) + + def complete_debug(self, text, line, begidx, endidx): + return self._complete_one_option(text, line, begidx, endidx, + ["off", "cli", "api", "all"]) diff --git a/targetcli/cli_config.py b/targetcli/cli_config.py new file mode 100644 index 0000000..5db702e --- /dev/null +++ b/targetcli/cli_config.py @@ -0,0 +1,706 @@ +''' +This file is part of the LIO SCSI Target. + +Copyright (c) 2012-2014 by Datera, Inc. +More information on www.datera.io. + +Original author: Jerome Martin <jxm@netiant.com> + +Datera and LIO are trademarks of Datera, Inc., which may be registered in some +jurisdictions. + +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 pyparsing as pp +import prettytable as pt +import os, sys, datetime, shutil + +from rtslib.config_filters import * +from targetcli.cli import Cli, CliError +from rtslib.config_live import dump_live +from targetcli.cli_logger import logger as log +from rtslib.config_parser import ConfigParser +from rtslib.config import Config, ConfigError + +# TODO Add path vs pattern documentation +# TODO Implement 'configure locked' mode +# TODO Implement do_copy +# TODO Implement do_comment +# TODO Implement do_rollback +# TODO When live summary is done, use tables for info +# TODO Allow PATH=='top ... ...' to indicate top-level + +class CliConfig(Cli): + ''' + The lio target configuration command-line for edit mode. + ''' + config_path = "/etc/target/scsi_target.lio" + history_path = os.path.expanduser("~/.targetcli/history_configure.txt") + + def __init__(self, interactive=False): + Cli.__init__(self, interactive, self.history_path) + self.set_prompt() + log.info("Syncing policy and configuration...") + self.backup_dir = "/var/target" + self.config = Config() + self.config.load_live() + self.edit_levels = [''] + self.needs_save = False + if interactive: + log.warning("[edit] top-level") + + @property + def needs_commit(self): + if self.needs_save: + return True + keys = ('removed', 'major', 'major_obj', + 'minor', 'minor_obj', 'created') + diff = self.config.diff_live() + for key in keys: + if diff[key]: + return True + return False + + @property + def attrs_missing(self): + for attr in self.config.current.walk(filter_only_missing): + return True + return False + + def add_edit_level(self, path): + self.edit_levels.append(path) + log.warning("[edit] %s" % self.edit_levels[-1]) + self.set_prompt(self.edit_levels[-1]) + + def del_edit_level(self): + if len(self.edit_levels) == 1: + raise CliError("Already at top-level") + + self.edit_levels.pop() + if len(self.edit_levels) == 1: + log.warning("[edit] top-level") + else: + log.warning("[edit] %s" % self.edit_levels[-1]) + self.set_prompt(self.edit_levels[-1]) + + def set_prompt(self, string=''): + ''' + Sets the prompt from string. + ''' + if not string: + prompt = "config# " + else: + max_len = 25 + if len(string) <= max_len: + prompt = "%s# " % string + else: + prompt = "..%s# " % string[-max_len+3:] + self.prompt = prompt + + def fmt_data_src(self, src): + + # TODO Get rid of this one in favor of lst_data_src + + def ts2str(ts): + date = datetime.datetime.fromtimestamp(int(ts)) + date = date.strftime('%Y-%m-%d %H:%M:%S') + return date + + try: + date = ts2str(src['timestamp']) + except: + date = "unknown date" + + if src['operation'] == 'set': + fmt = ("(%s) set %s" + % (date, src['data'].strip())) + elif src['operation'] == 'delete': + fmt = ("(%s) delete %s" + % (date, src['pattern'].strip())) + elif src['operation'] == 'load': + mdate = ts2str(src['mtime']) + fmt = ("(%s) load %s (modified %s)" + % (date, src['filepath'], mdate)) + elif src['operation'] == 'update': + mdate = ts2str(src['mtime']) + fmt = ("(%s) merge %s (modified %s)" + % (date, src['filepath'], mdate)) + elif src['operation'] == 'clear': + fmt = ("(%s) cleared config" + % date) + elif src['operation'] == 'resync': + fmt = ("(%s) Synchronized configuration with live system" + % date) + elif src['operation'] == 'init': + fmt = ("(%s) created new configuration" + % date) + else: + fmt = ("(%s) unknown operation" + % date) + return fmt + + def lst_data_src(self, src): + + def ts2str(ts): + date = datetime.datetime.fromtimestamp(int(ts)) + date = date.strftime('%Y-%m-%d %H:%M:%S') + return date + + try: + date = ts2str(src['timestamp']) + except: + date = "unknown date" + + if src['operation'] == 'set': + lst = [date, 'set', src['data'].strip()] + elif src['operation'] == 'delete': + lst = [date, 'delete', src['pattern'].strip()] + elif src['operation'] == 'load': + mdate = ts2str(src['mtime']) + lst = [date, 'load', + "%s\nmodified %s" % (src['filepath'], mdate)] + elif src['operation'] == 'update': + mdate = ts2str(src['mtime']) + lst = [date, 'merge', + "%s\nmodified %s" % (src['filepath'], mdate)] + elif src['operation'] == 'clear': + lst = [date, 'clear', 'n/a'] + elif src['operation'] == 'resync': + lst = [date, 'resync', 'n/a'] + elif src['operation'] == 'init': + lst = [date, 'init', 'n/a'] + else: + lst = [date, 'unknown', 'n/a'] + return lst + + def do_exit(self, options): + ''' + exit [now] + + Exits the current configuration edit level, and goes back to the + previous edit level. If run on the top-level configuration, then exits + config mode. + + If the now option is provided, no confirmation will be asked if there + are uncommitted changes in the current candidate configuration when + exiting the config mode. + ''' + options = self.parse(options, 'exit', pp.Optional('now'))[1:] + + if self.edit_levels[-1]: + self.del_edit_level() + exit = False + elif self.needs_commit: + log.warning("[edit] All non-commited changes will be lost!") + if 'now' in options: + log.warning("[edit] exiting anyway, as requested") + exit = True + else: + exit = self.yes_no("Exit config mode anyway?", False) + else: + exit = True + return exit + + def complete_exit(self, text, line, begidx, endidx): + return self._complete_options(text, line, begidx, endidx, ['now']) + + def do_commit(self, options): + ''' + commit [check|interactive] + + Saves the current configuration to the system startup configuration + file, after applying the changes to the running system. + + If the check option is provided, the current configuration will be + checked but not saved or applied. + + If the interactive option is provided, the user will be able to confirm + or skip every modification to the live system. + ''' + # TODO Add [as DESCRIPTION] option + # TODO Change to commit only current level unless 'all' option + syntax = pp.Optional(pp.oneOf("check interactive")) + options = self.parse(options, 'commit', syntax)[1:] + + if self.attrs_missing: + self.do_missing('') + raise CliError("Cannot validate configuration: " + "required attributes not set") + + if not self.needs_commit: + raise CliError("No changes to commit!") + + log.info("Validating configuration") + for msg in self.config.verify(): + log.info(msg) + if 'check' in options: + return + + do_it = self.yes_no("Apply changes and overwrite system " + "configuration ?", False) + if do_it is not False: + log.info("Applying configuration") + for msg in self.config.apply(): + if 'interactive' in options: + apply = self.yes_no("%s\nPlease confirm" % msg, True) + if apply is False: + log.warning("Aborted commit on user request: " + "please verify system status") + return + else: + log.info(msg) + + # TODO remove older backups + ts = datetime.datetime.now().strftime("%Y-%m-%d_%H:%M:%S") + backup_path = "%s/backup-%s.lio" % (self.backup_dir, ts) + log.info("Performing backup of startup configuration: %s" + % backup_path) + shutil.copyfile(self.config_path, backup_path) + log.info("Saving new startup configuration") + # We reload the config from live before saving it, in + # case this kernel has new attributes not yet in our + # policy files + self.config.load_live() + self.config.save(self.config_path) + self.needs_save = False + else: + log.info("Cancelled configuration commit") + + def complete_commit(self, text, line, begidx, endidx): + return self._complete_options(text, line, begidx, endidx, + ['check', 'interactive']) + + def do_rollback(self, options): + ''' + rollback + + Return to the last committed configuration. Only the current + configuration is affected. The commit command can then be used to apply + the rolled-back configuration to the running system. + ''' + # TODO Add more control to directly rollback the n-th version, view + # backup infos before rollback, etc. + backups = sorted(n for n in os.listdir(self.backup_dir) + if n.endswith(".lio")) + if not backups: + raise ConfigError("No backup found") + else: + backup_path = "%s/%s" % (self.backup_dir, backups[-1]) + self.config.load(backup_path) + os.remove(backup_path) + log.warning("Rolled-back to %s" % backup_path) + + def do_edit(self, options): + ''' + edit PATH + + Changes the current configuration edit level to PATH, relative to the + current configuration edit level. If PATH does not exist currently, it + will be created. + ''' + level = self.edit_levels[-1] + nodes = self.config.search("%s %s" % (level, options)) + if not nodes: + nodes_beyond = self.config.search("%s %s .*" % (level, options)) + if nodes_beyond: + raise CliError("Incomplete path: [%s]" % options) + else: + statement = "%s %s" % (self.edit_levels[-1], options) + log.debug("Setting statement '%s'" % statement) + self.config.set(statement) + self.needs_save = True + node = self.config.search(statement)[0] + log.info("Created configuration level: %s" % node.path_str) + self.add_edit_level(node.path_str) + self.do_missing('') + elif len(nodes) > 1: + raise CliError("Ambiguous path: [%s]" % options) + else: + self.add_edit_level(nodes[0].path_str) + self.do_missing('') + + def complete_edit(self, text, line, begidx, endidx): + # TODO Add tips for new path + return self._complete_path(text, line, begidx, endidx, + self.edit_levels[-1]) + + def do_live(self, options): + ''' + live COMMAND + + Executes a single non-interactive command in live mode. + ''' + # TODO Add completion + from targetcli.cli_live import CliLive + CliLive(interactive=False).onecmd(options) + + def do_set(self, options): + ''' + set [PATH] OBJECT IDENTIFIER + set [PATH] ATTRIBUTE VALUE + + Sets either an OBJECT IDENTIFIER (i.e. "disk mydisk") or an ATTRIBUTE + VALUE (i.e. "enable yes"). + ''' + if not options: + raise CliError("Missing required options") + statement = "%s %s" % (self.edit_levels[-1], options) + log.debug("Setting statement '%s'" % statement) + created = self.config.set(statement) + for node in created: + log.info("[%s] has been set" % node.path_str) + if not created: + log.info("Ignored: Current configuration already match statement") + else: + self.needs_save = True + + def complete_set(self, text, line, begidx, endidx): + # TODO Add tips for new path + return self._complete_path(text, line, begidx, endidx, + self.edit_levels[-1]) + + def do_delete(self, options): + ''' + delete [PATH] + + Deletes either all LIO configuration objects at the current edit level, + or only those under PATH relative to the current level. + ''' + path = "%s %s" % (self.edit_levels[-1], options) + if not path.strip(): + raise CliError("Cannot delete top-level configuration") + + nodes = self.config.search(path) + if not nodes: + # TODO Replace all "%s .*" forms with a try_hard arg to search + nodes.extend(self.config.search("%s .*" % path)) + if not nodes: + raise CliError("No configuration objects at path: %s" + % path.strip()) + + # FIXME Use a real tree walk with filter + obj_no = 0 + for node in nodes: + if node.data['type'] == 'obj': + obj_no +=1 + + if obj_no == 0: + raise CliError("Can't delete attributes, only objects: %s" + % path.strip()) + + do_it = self.yes_no("Delete %d objects(s) from current configuration?" + % len(nodes), False) + if do_it is not False: + deleted = self.config.delete(path) + if not deleted: + deleted = self.config.delete("%s .*" % path) + self.needs_save = True + log.info("Deleted %d configuration object(s)" % obj_no) + else: + log.info("Cancelled: configuration not modified") + + def complete_delete(self, text, line, begidx, endidx): + # TODO Filter for objects only, skip attributes + return self._complete_path(text, line, begidx, endidx, + self.edit_levels[-1]) + + def do_undo(self, options): + ''' + undo + + Undo the last configuration change done during this config mode + session. The lio cli has unlimited undo levels capabilities within a + session. + + To restore a previously commited configuration, see the rollback + command. + ''' + options = self.parse(options, 'undo', '')[1:] + data_src = self.config.current.data['source'] + self.config.undo() + self.needs_save = True + + # TODO Implement info option to view all previous ops + # TODO Implement last N option for multiple undo + + log.info("[undo] %s" % self.fmt_data_src(data_src)) + + def do_info(self, options): + ''' + info [PATH] + + Displays edit history information about the current configuration level + or all configuration items matching PATH. + ''' + # TODO Add node type information + path = "%s %s" % (self.edit_levels[-1], options) + if not path.strip(): + # This is just a test for tables + table = pt.PrettyTable() + table.hrules = pt.ALL + table.field_names = ["change", "date", "type", "data"] + table.align['data'] = 'l' + changes = [] + nb_ver = len(self.config._configs) + for idx, cfg in enumerate(reversed(self.config._configs)): + lst_src = self.lst_data_src(cfg.data['source']) + table.add_row(["%03d" % (idx + 1)] + lst_src) + # FIXME Use term width to compute these + table.max_width["date"] = 10 + table.max_width["data"] = 43 + sys.stdout.write("%s\n" % table.get_string()) + else: + nodes = self.config.search(path) + if not nodes: + # TODO Replace all "%s .*" forms with a try_hard arg to search + nodes.extend(self.config.search("%s .*" % path)) + if not nodes: + raise CliError("Path does not exist: %s" % path.strip()) + infos = [] + for node in nodes: + if node.data.get('required'): + req = "(required attribute) " + else: + req = "" + path = node.path_str + infos.append("%s[%s]\nLast change: %s" + % (req, path, + self.fmt_data_src(node.data['source']))) + log.info("\n\n".join(infos)) + + def complete_info(self, text, line, begidx, endidx): + return self._complete_path(text, line, begidx, endidx, + self.edit_levels[-1]) + + def do_clear(self, options): + ''' + clear + + Clears the current configuration. This removes all current objects and + attributes from the configuration. + ''' + options = self.parse(options, 'clear', '')[1:] + + self.config.clear() + log.info("Configuration cleared") + + def do_load(self, options): + ''' + load live|FILE_PATH + + Replaces the current configuration with the contents of FILE_PATH. + If any error happens while doing so, the current configuration will + be fully rolled back. + + If live is used instead of FILE_PATH, the configuration from the live + system will be used instead. + ''' + # TODO Add completion for filepath + # TODO Add a filepath type to policy and also a parser we can use here + tok_string = (pp.QuotedString('"') + | pp.QuotedString("'") + | pp.Word(pp.printables, excludeChars="{}#'\";")) + options = self.parse(options, 'load', tok_string)[1:] + src = options[0] + if src == 'live': + if self.yes_no("Replace the current configuration with the " + "running configuration?", False) is not False: + self.config.load_live() + else: + log.info("Cancelled: configuration not modified") + else: + if self.yes_no("Replace the current configuration with %s?" + % src, False) is not False: + self.config.load(src) + else: + log.info("Cancelled: configuration not modified") + + def complete_load(self, text, line, begidx, endidx): + # TODO Add filename support + return self._complete_options(text, line, begidx, endidx, ['live']) + + def do_merge(self, options): + ''' + merge live|FILE_PATH + + Merges the contents of FILE_PATH with the current configuration. + In case of conflict, values from FILE_PATH will be used. + If any error happens while doing so, the current configuration will + be fully rolled back. + + If live is used instead of FILE_PATH, the configuration from the live + system will be used instead. + ''' + # TODO Add completion for filepath + # TODO Add a filepath type to policy and also a parser we can use here + tok_string = (pp.QuotedString('"') + | pp.QuotedString("'") + | pp.Word(pp.printables, excludeChars="{}#'\";")) + options = self.parse(options, 'merge', tok_string)[1:] + src = options[0] + if src == 'live': + if self.yes_no("Merge the running configuration with " + "the current configuration?", False) is not False: + self.config.set(dump_live()) + else: + log.info("Cancelled: configuration not modified") + else: + if self.yes_no("Merge %s with the current configuration?" + % src, False) is not False: + self.config.update(src) + else: + log.info("Cancelled: configuration not modified") + + def complete_merge(self, text, line, begidx, endidx): + # TODO Add filename support + return self._complete_options(text, line, begidx, endidx, ['live']) + + def do_dump(self, options): + ''' + dump FILE_PATH [PATH|all] + + Dumps a copy of either the current configuration level or the + configuration at PATH to FILE_PATH. If PATH is 'all', then the + top-level configuration will be dumped. + ''' + options = options.split() + if len(options) < 1: + raise CliError("Syntax error: expected at least one option") + filepath = options.pop(0) + if not filepath.startswith('/'): + raise CliError("Expected an absolute file path") + path = " ".join(options) + if path.strip() == 'all': + path = '' + else: + path = ("%s %s" % (self.edit_levels[-1], path)).strip() + + self.config.save(filepath, path) + if not path: + path_desc = 'all' + else: + path_desc = path + # FIXME Accept "half-node" path + log.info("Dumped [%s] to %s" % (path_desc, filepath)) + + def complete_dump(self, text, line, begidx, endidx): + options = line.split()[1:] + if len(options) < 1: + return self._complete_filepath(text, options[0], + begidx, endidx) + else: + # FIXME This is broken + return self._complete_path(text, " ".join(options[1:]), + begidx, endidx, self.edit_levels[-1]) + + def do_show(self, options): + ''' + show [all] [PATH] + + Shows the current candidate configuration for PATH, relative to the + current edit level. + + Note that attributes with default values will be + filrered out by default, unless the all option is used. + ''' + if options and options.split()[0] == 'all': + options = " ".join(options.split()[1:]) + node_filter = lambda x:x + else: + node_filter = filter_no_default + + path = ("%s %s" % (self.edit_levels[-1], options)).strip() + config = self.config.dump(path, node_filter) + if config is None: + config = self.config.dump("%s .*" % path, node_filter) + if config is not None: + sys.stdout.write("%s\n" % config) + else: + log.error("No such path in current configuration: %s" % path) + + def complete_show(self, text, line, begidx, endidx): + # TODO add all option + return self._complete_path(text, line, begidx, endidx, + self.edit_levels[-1]) + + def do_missing(self, options): + ''' + missing [PATH] + + Shows all missing required attribute values in the current candidate + configuration for PATH, relative to the current edit level. + ''' + node_filter = filter_only_missing + path = ("%s %s" % (self.edit_levels[-1], options)).strip() + if not path: + path = '.*' + trees = self.config.search(path) + if not trees: + trees = self.config.search("%s .*" % path) + if not trees: + raise CliError("No such path: %s" % path) + + missing = [] + for tree in trees: + for attr in tree.walk(node_filter): + missing.append(attr) + + if not options: + path = "current configuration" + + if not missing: + log.warning("No missing attributes values under %s" % path) + else: + log.warning("Missing attributes values under %s:" % path) + for attr in missing: + log.info(" %s" % attr.path_str) + sys.stdout.write("\n") + + def complete_missing(self, text, line, begidx, endidx): + return self._complete_path(text, line, begidx, endidx, + self.edit_levels[-1]) + + def do_diff(self, options): + ''' + diff + + Shows all differences between the current configuration and the live + running configuration. + ''' + options = self.parse(options, 'diff', '')[1:] + diff = self.config.diff_live() + has_diffs = False + if diff['removed']: + has_diffs = True + log.warning("Objects removed in the current configuration:") + for node in diff['removed']: + log.info(" %s" % node.path_str) + if diff['created']: + has_diffs = True + log.warning("New objects in the current configuration:") + for node in diff['created']: + log.info(" %s" % node.path_str) + if diff['major']: + has_diffs = True + log.warning("Major attribute changes in the current configuration:") + for node in diff['major']: + log.info(" %s" % node.path_str) + if diff['minor']: + has_diffs = True + log.warning("Minor attribute changes in the current configuration:") + for node in diff['minor']: + log.info(" %s" % node.path_str) + if not has_diffs: + log.warning("Current configuration is in sync with live system") + else: + sys.stdout.write("\n") diff --git a/targetcli/cli_live.py b/targetcli/cli_live.py new file mode 100644 index 0000000..5c9d6da --- /dev/null +++ b/targetcli/cli_live.py @@ -0,0 +1,141 @@ +''' +This file is part of the LIO SCSI Target. + +Copyright (c) 2012-2014 by Datera, Inc. +More information on www.datera.io. + +Original author: Jerome Martin <jxm@netiant.com> + +Datera and LIO are trademarks of Datera, Inc., which may be registered in some +jurisdictions. + +Licensed under the Apache License, Version 2.0 (the "License"); you may +not use this file except in compliance with the License. You may obtain +a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +License for the specific language governing permissions and limitations +under the License. +''' +import os, sys +import pyparsing as pp + +from rtslib.config_filters import * +from targetcli.cli import Cli, CliError +from targetcli.cli_config import CliConfig +from targetcli.cli_logger import logger as log +from rtslib.config import Config, ConfigError + +# TODO Implement do_summary using tables + color +# TODO Implement sum for PR +# TODO Implement sum for initiator sessions +# TODO Implement sum for alua metadata +# TODO Implement sum + mgmt for fabric modules +# TODO Implement sum for network BW + portals +# TODO Implement sum for disk IO + +class CliLive(Cli): + ''' + The lio target configuration command-line for live mode. + ''' + history_path = os.path.expanduser("~/.targetcli/history_live.txt") + intro = ("\nWelcome to the lio target interactive shell.\n" + "Copyright (c) 2012-2014 by Datera, Inc.\n" + "Enter '?' to list available commands.\n") + + def __init__(self, interactive=False): + Cli.__init__(self, interactive, self.history_path) + self.prompt = "live> " + self.do_resync() + + def do_exit(self, options): + ''' + exit + + Exits the lio target configuration shell. + ''' + options = self.parse(options, 'exit', '') + return True + + def do_resync(self, options=''): + ''' + resync + + Re-synchronizes the cli with the live running configuration. This + could be useful in rare cases where manual changes have been made to + the underlying configfs structure for debugging purposes. + ''' + options = self.parse(options, 'resync', '') + log.info("Syncing policy and configuration...") + # FIXME Investigate bug in ConfigTree code: error if loading live twice + # without recreating the Config object. + self.config = Config() + self.config.load_live() + + def do_configure(self, options): + ''' + configure + + Switch to config mode. In this mode, you can safely edit a candidate + configuration for the system, and commit it only when it is ready. + ''' + options = self.parse(options, 'configure', '') + if not self.interactive: + raise CliError("Cannot switch to config mode when running " + "non-interactively.") + else: + self.save_history() + self.clear_history() + # FIXME Preserve CliConfig session state, notably undo history + CliConfig(interactive=True).cmdloop() + self.clear_history() + self.load_history() + self.do_resync() + log.warning("[live] Back to live mode") + + def do_show(self, options): + ''' + show [all] [PATH] + + Shows the running live configuration for PATH. + + Note that attributes with default values will be + filrered out by default, unless the all option is used. + ''' + if options and options.split()[0] == 'all': + options = " ".join(options.split()[1:]) + node_filter = lambda x:x + else: + node_filter = filter_no_default + + config = self.config.dump(options, node_filter) + if config is None: + config = self.config.dump("%s .*" % options, node_filter) + if config is not None: + sys.stdout.write("%s\n" % config) + else: + log.error("No such path in current configuration: %s" % options) + + def complete_show(self, text, line, begidx, endidx): + # TODO add all option + return self._complete_path(text, line, begidx, endidx) + + def do_initialize_system(self, options): + ''' + initialize_system + + Loads and commits the system startup configuration if it exists. + ''' + self.config.load(CliConfig.config_path) + do_it = self.yes_no("Load and commit the system startup configuration?" + , False) + if do_it is not False: + log.info("Initializing LIO target...") + for msg in self.config.apply(): + log.info(msg) + self.config.load_live() + diff --git a/targetcli/cli_logger.py b/targetcli/cli_logger.py new file mode 100644 index 0000000..78e5687 --- /dev/null +++ b/targetcli/cli_logger.py @@ -0,0 +1,48 @@ +''' +This file is part of the LIO SCSI Target. + +Copyright (c) 2012-2014 by Datera, Inc. +More information on www.datera.io. + +Original author: Jerome Martin <jxm@netiant.com> + +Datera and LIO are trademarks of Datera, Inc., which may be registered in some +jurisdictions. + +Licensed under the Apache License, Version 2.0 (the "License"); you may +not use this file except in compliance with the License. You may obtain +a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +License for the specific language governing permissions and limitations +under the License. +''' +import sys, logging + +class LogFormatter(logging.Formatter): + + default_format = "LOG%(levelno)s: %(msg)s" + formats = {10: "DEBUG:%(module)s:%(lineno)s: %(msg)s", + 20: "%(msg)s", + 30: "\n### %(msg)s\n", + 40: "*** %(msg)s", + 50: "CRITICAL: %(msg)s"} + + def __init__(self): + logging.Formatter.__init__(self) + + def format(self, record): + self._fmt = self.formats.get(record.levelno, self.default_format) + return logging.Formatter.format(self, record) + +logger = logging.getLogger("LioCli") +logger.setLevel(logging.INFO) + +log_fmt = LogFormatter() +log_handler = logging.StreamHandler(sys.stdout) +log_handler.setFormatter(log_fmt) +logging.root.addHandler(log_handler) diff --git a/targetcli/ui_backstore.py b/targetcli/ui_backstore.py index 43bc41f..4d4b2bd 100644 --- a/targetcli/ui_backstore.py +++ b/targetcli/ui_backstore.py @@ -2,7 +2,7 @@ Implements the targetcli backstores related UI. This file is part of targetcli. -Copyright (c) 2011-2013 by Datera, Inc +Copyright (c) 2011-2014 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 @@ -16,14 +16,15 @@ WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ''' - +import os from ui_node import UINode, UIRTSLibNode from rtslib import RTSRoot from rtslib import FileIOBackstore, IBlockBackstore -from rtslib import PSCSIBackstore, RDDRBackstore, RDMCPBackstore +from rtslib import PSCSIBackstore, RDMCPBackstore from rtslib import FileIOStorageObject, IBlockStorageObject -from rtslib import PSCSIStorageObject, RDDRStorageObject, RDMCPStorageObject +from rtslib import PSCSIStorageObject, RDMCPStorageObject from rtslib.utils import get_block_type, is_disk_partition +from rtslib.utils import convert_human_to_bytes, convert_bytes_to_human from configshell import ExecutionError def dedup_so_name(storage_object): @@ -52,15 +53,14 @@ class UIBackstores(UINode): def refresh(self): self._children = set([]) UIPSCSIBackstore(self) - UIRDDRBackstore(self) UIRDMCPBackstore(self) UIFileIOBackstore(self) UIIBlockBackstore(self) - class UIBackstore(UINode): ''' A backstore UI. + Abstract Base Class, do not instantiate. ''' def __init__(self, plugin, parent): UINode.__init__(self, plugin, parent) @@ -92,7 +92,7 @@ class UIBackstore(UINode): return generate_wwn def prm_buffered(self, buffered): - generate_wwn = \ + buffered = \ self.ui_eval_param(buffered, 'bool', True) if buffered: self.shell.log.info("Using buffered mode.") @@ -119,7 +119,7 @@ class UIBackstore(UINode): else: hba = child.rtsnode.backstore child.rtsnode.delete() - if not hba.storage_objects: + if not list(hba.storage_objects): hba.delete() self.remove_child(child) self.shell.log.info("Deleted storage object %s." % name) @@ -194,6 +194,11 @@ class UIPSCSIBackstore(UIBackstore): self.assert_root() self.assert_available_so_name(name) backstore = PSCSIBackstore(self.next_hba_index(), mode='create') + + if get_block_type(dev) is not None or is_disk_partition(dev): + self.shell.log.info("Note: block backstore recommended for " + "SCSI block devices") + try: so = PSCSIStorageObject(backstore, name, dev) except Exception, exception: @@ -204,47 +209,6 @@ class UIPSCSIBackstore(UIBackstore): % (name, dev)) return self.new_node(ui_so) - -class UIRDDRBackstore(UIBackstore): - ''' - RDDR backstore UI. - ''' - def __init__(self, parent): - UIBackstore.__init__(self, 'rd_dr', parent) - - def ui_command_create(self, name, size, generate_wwn=None): - ''' - Creates an RDDR storage object. I{size} is the size of the ramdisk, and - the optional I{generate_wwn} parameter is a boolean specifying whether - or not we should generate a T10 wwn serial for the unit (by default, - yes). - - 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() - self.assert_available_so_name(name) - backstore = RDDRBackstore(self.next_hba_index(), mode='create') - try: - so = RDDRStorageObject(backstore, name, size, - self.prm_gen_wwn(generate_wwn)) - - except Exception, exception: - backstore.delete() - raise exception - ui_so = UIStorageObject(so, self) - self.shell.log.info("Created rd_dr ramdisk %s with size %s." - % (name, size)) - return self.new_node(ui_so) - - class UIRDMCPBackstore(UIBackstore): ''' RDMCP backstore UI. @@ -252,7 +216,7 @@ class UIRDMCPBackstore(UIBackstore): def __init__(self, parent): UIBackstore.__init__(self, 'rd_mcp', parent) - def ui_command_create(self, name, size, generate_wwn=None): + def ui_command_create(self, name, size, generate_wwn=None, nullio=None): ''' Creates an RDMCP storage object. I{size} is the size of the ramdisk, and the optional I{generate_wwn} parameter is a boolean specifying @@ -272,9 +236,11 @@ class UIRDMCPBackstore(UIBackstore): self.assert_root() self.assert_available_so_name(name) backstore = RDMCPBackstore(self.next_hba_index(), mode='create') + nullio = self.ui_eval_param(nullio, 'bool', False) try: so = RDMCPStorageObject(backstore, name, size, - self.prm_gen_wwn(generate_wwn)) + self.prm_gen_wwn(generate_wwn), + nullio=nullio) except Exception, exception: backstore.delete() @@ -282,6 +248,9 @@ class UIRDMCPBackstore(UIBackstore): ui_so = UIStorageObject(so, self) self.shell.log.info("Created rd_mcp ramdisk %s with size %s." % (name, size)) + if nullio and not so.nullio: + self.shell.log.warning("nullio ramdisk is not supported by this " + "kernel version, created with nullio=false") return self.new_node(ui_so) @@ -292,8 +261,26 @@ class UIFileIOBackstore(UIBackstore): def __init__(self, parent): UIBackstore.__init__(self, 'fileio', parent) + def _create_file(self, filename, size, sparse=True): + f = open(filename, "w+") + try: + if sparse: + os.ftruncate(f.fileno(), size) + else: + self.shell.log.info("Writing %s bytes" % size) + while size > 0: + write_size = min(size, 1024) + f.write("\0" * write_size) + size -= write_size + except IOError: + f.close() + os.remove(filename) + raise ExecutionError("Could not expand file to size") + f.close() + def ui_command_create(self, name, file_or_dev, size=None, - generate_wwn=None, buffered=None): + generate_wwn=None, buffered=None, sparse=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 @@ -303,8 +290,11 @@ class UIFileIOBackstore(UIBackstore): a block device. The optional I{generate_wwn} parameter is a boolean specifying whether or not we should generate a T10 wwn Serial for the unit (by default, yes). The I{buffered} parameter is a boolean stating - whether or not to enable buffered mode. It is disabled by default - (synchronous mode). + whether or not to enable buffered mode. It is enabled by default + (asynchronous mode). 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 =========== @@ -319,10 +309,16 @@ class UIFileIOBackstore(UIBackstore): self.assert_root() self.assert_available_so_name(name) self.shell.log.debug("Using params size=%s generate_wwn=%s buffered=%s" - % (size, generate_wwn, buffered)) + " sparse=%s" + % (size, generate_wwn, buffered, sparse)) + + sparse = self.ui_eval_param(sparse, 'bool', True) + + backstore = FileIOBackstore(self.next_hba_index(), mode='create') + is_dev = get_block_type(file_or_dev) is not None \ or is_disk_partition(file_or_dev) - + if size is None and is_dev: backstore = FileIOBackstore(self.next_hba_index(), mode='create') try: @@ -335,6 +331,8 @@ class UIFileIOBackstore(UIBackstore): raise exception self.shell.log.info("Created fileio %s with size %s." % (name, size)) + self.shell.log.info("Note: block backstore preferred for " + " best results.") ui_so = UIStorageObject(so, self) return self.new_node(ui_so) elif size is not None and not is_dev: @@ -352,8 +350,23 @@ class UIFileIOBackstore(UIBackstore): ui_so = UIStorageObject(so, self) return self.new_node(ui_so) else: - self.shell.log.error("For fileio, you must either specify both a " - + "file and a size, or just a device path.") + # use given file size only if backing file does not exist + if os.path.isfile(file_or_dev): + new_size = str(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") + self._create_file(file_or_dev, convert_human_to_bytes(size), + sparse) class UIIBlockBackstore(UIBackstore): @@ -388,6 +401,7 @@ class UIIBlockBackstore(UIBackstore): class UIStorageObject(UIRTSLibNode): ''' A storage object UI. + Abstract Base Class, do not instantiate. ''' def __init__(self, storage_object, parent): name = storage_object.name @@ -417,17 +431,25 @@ class UIStorageObject(UIRTSLibNode): legacy = [] if self.rtsnode.name != self.name: legacy.append("ADDED SUFFIX") - if len(self.rtsnode.backstore.storage_objects) > 1: + if len(list(self.rtsnode.backstore.storage_objects)) > 1: legacy.append("SHARED HBA") if legacy: errors.append("LEGACY: " + ", ".join(legacy)) + size = convert_bytes_to_human(getattr(so, "size", 0)) + nullio_str = "" + try: + if so.nullio: + nullio_str = " (nullio)" + except AttributeError: + pass + if errors: msg = ", ".join(errors) if path: msg += " (%s %s)" % (path, so.status) return (msg, False) else: - return ("%s %s" % (path, so.status), True) + return ("%s %s%s%s" % (path, size, so.status, nullio_str), True) diff --git a/targetcli/ui_backstore_legacy.py b/targetcli/ui_backstore_legacy.py index 2d46fcd..514cabc 100644 --- a/targetcli/ui_backstore_legacy.py +++ b/targetcli/ui_backstore_legacy.py @@ -2,7 +2,7 @@ Implements the targetcli backstores related UI. his file is part of targetcli. -Copyright (c) 2011-2013 by Datera, Inc +Copyright (c) 2011-2014 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 @@ -20,9 +20,9 @@ under the License. from ui_node import UINode, UIRTSLibNode from rtslib import RTSRoot from rtslib import FileIOBackstore, IBlockBackstore -from rtslib import PSCSIBackstore, RDDRBackstore, RDMCPBackstore +from rtslib import PSCSIBackstore, RDMCPBackstore from rtslib import FileIOStorageObject, IBlockStorageObject -from rtslib import PSCSIStorageObject, RDDRStorageObject, RDMCPStorageObject +from rtslib import PSCSIStorageObject, RDMCPStorageObject from rtslib.utils import get_block_type, is_disk_partition class UIBackstoresLegacy(UINode): @@ -40,8 +40,6 @@ class UIBackstoresLegacy(UINode): backstore_plugin = backstore.plugin if backstore_plugin == 'pscsi': UIPSCSIBackstoreLegacy(backstore, self) - elif backstore_plugin == 'rd_dr': - UIRDDRBackstoreLegacy(backstore, self) elif backstore_plugin == 'rd_mcp': UIRDMCPBackstoreLegacy(backstore, self) elif backstore_plugin == 'fileio': @@ -92,18 +90,11 @@ class UIBackstoresLegacy(UINode): block I/O with various methods (synchronous or asynchronous) and (buffered or direct). - B{rd_dr} - ------- - This I{backstore_plugin} provides the same level of SCSI emulation than - the I{fileio} and I{iblock} backstores, but uses a B{ramdisk}, based on - direct memory mapping. It is the fastest of all backstores, and is - typically used for bandwidth testing. - B{rd_mcp} -------- - This I{backstore_plugin} is a bit slower than B{rd_dr}, but more robust - with multiple initiators, with a separate memory mapping using memory - copy. Also typically used for bandwidth testing. + This I{backstore_plugin} uses a ramdisk with a separate + mapping using memory copy. Typically used for bandwidth + testing. EXAMPLE ======= @@ -134,9 +125,6 @@ class UIBackstoresLegacy(UINode): if backstore_plugin == 'pscsi': backstore = PSCSIBackstore(backstore_index, mode='create') return self.new_node(UIPSCSIBackstoreLegacy(backstore, self)) - elif backstore_plugin == 'rd_dr': - backstore = RDDRBackstore(backstore_index, mode='create') - return self.new_node(UIRDDRBackstoreLegacy(backstore, self)) elif backstore_plugin == 'rd_mcp': backstore = RDMCPBackstore(backstore_index, mode='create') return self.new_node(UIRDMCPBackstoreLegacy(backstore, self)) @@ -166,7 +154,7 @@ class UIBackstoresLegacy(UINode): @rtype: list of str ''' if current_param == 'backstore_plugin': - plugins = ['pscsi', 'rd_dr', 'rd_mcp', 'fileio', 'iblock'] + plugins = ['pscsi', 'rd_mcp', 'fileio', 'iblock'] completions = [plugin for plugin in plugins if plugin.startswith(text)] else: @@ -339,37 +327,6 @@ class UIPSCSIBackstoreLegacy(UIBackstoreLegacy): % (name, dev)) return self.new_node(ui_so) - -class UIRDDRBackstoreLegacy(UIBackstoreLegacy): - ''' - RDDR backstore UI. - ''' - def ui_command_create(self, name, size, generate_wwn=None): - ''' - Creates an RDDR storage object. I{size} is the size of the ramdisk, and - the optional I{generate_wwn} parameter is a boolean specifying whether - or not we should generate a T10 wwn serial for the unit (by default, - yes). - - 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() - so = RDDRStorageObject(self.rtsnode, name, size, - self.prm_gen_wwn(generate_wwn)) - ui_so = UIStorageObjectLegacy(so, self) - self.shell.log.info("Created rd_dr ramdisk %s with size %s." - % (name, size)) - return self.new_node(ui_so) - - class UIRDMCPBackstoreLegacy(UIBackstoreLegacy): ''' RDMCP backstore UI. diff --git a/targetcli/ui_node.py b/targetcli/ui_node.py index 52079c1..9b90feb 100644 --- a/targetcli/ui_node.py +++ b/targetcli/ui_node.py @@ -2,7 +2,7 @@ Implements the targetcli base UI node. This file is part of targetcli. -Copyright (c) 2011-2013 by Datera, Inc +Copyright (c) 2011-2014 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 @@ -18,11 +18,13 @@ under the License. ''' from configshell import ConfigNode, ExecutionError -from rtslib import RTSLibError, RTSRoot +from rtslib import RTSLibError, RTSRoot, Config from subprocess import PIPE, Popen from os.path import isfile from os import getuid +STARTUP_CONFIG = "/etc/target/scsi_target.lio" + def exec3(cmd): ''' Executes a shell command **cmd** and returns @@ -44,8 +46,8 @@ class UINode(ConfigNode): ConfigNode.__init__(self, name, parent, shell) self.cfs_cwd = RTSRoot.configfs_dir self.define_config_group_param( - 'global', 'auto_enable_tpgt', 'bool', - 'If true, automatically enables TPGTs upon creation.') + 'global', 'auto_enable_tpg', 'bool', + 'If true, automatically enables TPGs upon creation.') self.define_config_group_param( 'global', 'auto_add_mapped_luns', 'bool', 'If true, automatically create node ACLs mapped LUNs ' @@ -64,7 +66,7 @@ class UINode(ConfigNode): 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: + if hasattr(root_node, 'as_root') and not root_node.as_root: raise ExecutionError("This privileged command is disabled: " + "you are not root.") @@ -100,7 +102,7 @@ class UINode(ConfigNode): result = ConfigNode.execute_command(self, command, pparams, kparams) except RTSLibError, msg: - self.shell.log.error(msg) + self.shell.log.error(str(msg)) else: self.shell.log.debug("Command %s succeeded." % command) return result @@ -109,34 +111,30 @@ class UINode(ConfigNode): ''' Exits the command line interface. ''' - config_needs_save = False - config_paths = {'tcm': "/etc/target/tcm_start.sh", - 'lio': "/etc/target/lio_start.sh"} - for mod_name, config_path in config_paths.items(): - saved_config = '' - live_config = exec3("%s_dump --stdout" % mod_name)[1] - if isfile(config_path): - with open(config_path) as config_fh: - saved_config = config_fh.read() + if getuid() == 0: + config = Config() + if isfile(STARTUP_CONFIG): + config.load(STARTUP_CONFIG, allow_new_attrs=True) + saved_config = config.dump() + config.load_live() + live_config = config.dump() if saved_config != live_config: - config_needs_save = True - break - - if config_needs_save and getuid() == 0: - self.shell.con.display("There are unsaved configuration changes.\n" - "If you exit now, configuration will not " - "be updated and changes will be lost upon " - "reboot.") - try: - input = raw_input("Type 'exit' if you want to exit anyway: ") - except EOFError: - input = None - self.shell.con.display('') - if input == "exit": - return 'EXIT' + self.shell.con.display("There are unsaved configuration changes.\n" + "If you exit now, configuration will not " + "be updated and changes will be lost upon " + "reboot.") + try: + input = raw_input("Type 'exit' if you want to exit anyway: ") + except EOFError: + input = None + self.shell.con.display('') + if input == "exit": + return 'EXIT' + else: + self.shell.log.warning("Aborted exit, use 'saveconfig' to " + "save the current configuration.") else: - self.shell.log.warning("Aborted exit, use 'saveconfig' to " - "save the current configuration.") + return 'EXIT' else: return 'EXIT' @@ -206,14 +204,16 @@ class UIRTSLibNode(UINode): Overrides the parent's execute_command() to check if the underlying RTSLib object still exists before returning. ''' - if not self.rtsnode.exists: + try: + self.rtsnode._check_self() + except RTSLibError: self.shell.log.error("The underlying rtslib object for " + "%s does not exist." % self.path) root = self.get_root() root.refresh() return root - else: - return UINode.execute_command(self, command, pparams, kparams) + + return UINode.execute_command(self, command, pparams, kparams) def ui_getgroup_attribute(self, attribute): ''' diff --git a/targetcli/ui_root.py b/targetcli/ui_root.py index deda9f0..a55c20d 100644 --- a/targetcli/ui_root.py +++ b/targetcli/ui_root.py @@ -2,7 +2,7 @@ Implements the targetcli root UI. This file is part of targetcli. -Copyright (c) 2011-2013 by Datera, Inc +Copyright (c) 2011-2014 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 @@ -16,13 +16,13 @@ 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 os import system -from rtslib import RTSRoot -from ui_node import UINode +import readline, tempfile +from rtslib import RTSRoot, Config +from ui_node import UINode, STARTUP_CONFIG from socket import gethostname +from cli_config import CliConfig from ui_target import UIFabricModule -from tcm_dump import tcm_full_backup from ui_backstore import UIBackstores from ui_backstore_legacy import UIBackstoresLegacy @@ -89,10 +89,45 @@ class UIRoot(UINode): input = None self.shell.con.display('') if input == "yes": - tcm_full_backup(None, None, '1', None) + config = Config() + config.load_live() + with open(STARTUP_CONFIG, "w") as fd: + fd.write(config.dump()) else: self.shell.log.warning("Aborted, configuration left untouched.") + def ui_command_configure(self): + ''' + Enters the config mode. + + This mode allows editing a candidate configuration without + impacting the running system. This candidate configuration can + then either be commited or discarded at will. If commited, it + will be applied to the running system and saved as the new + startup configuration. + + Other features include loading a configuration from file, undo + support, rollback support, configuration backups and more. + + This mode is a functionnal but early preview version of the next- + generation targetcli environment. + ''' + self.assert_root() + self.shell.log.warning("Entering configure mode") + self.shell.log.warning("This mode is a functionnal but early " + "preview version of the next-generation " + "targetcli") + #tmp_fd = tempfile.NamedTemporaryFile() + #tmp_history = tmp_fd.name + #tmp_fd.close() + #readline.write_history_file(tmp_history) + #readline.clear_history() + #CliConfig(interactive=True).cmdloop() + #readline.clear_history() + #readline.read_history_file(tmp_history) + system("targetcli-ng configure") + self.refresh() + def ui_command_version(self): ''' Displays the targetcli and support libraries versions. diff --git a/targetcli/ui_target.py b/targetcli/ui_target.py index 8f27278..cd4efed 100644 --- a/targetcli/ui_target.py +++ b/targetcli/ui_target.py @@ -2,7 +2,7 @@ Implements the targetcli target related UI. This file is part of targetcli. -Copyright (c) 2011-2013 by Datera, Inc +Copyright (c) 2011-2014 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 @@ -255,7 +255,7 @@ class UIMultiTPGTarget(UIRTSLibNode): ''' Creates a new Target Portal Group within the target. The I{tag} must be a strictly positive integer value. If omitted, the next available - Target Portal Group Tag (TPGT) will be used. + Target Portal Group Tag (TPG) will be used. SEE ALSO ======== @@ -280,30 +280,32 @@ class UIMultiTPGTarget(UIRTSLibNode): self.shell.log.error("The TPG Tag must be an integer value.") return else: - if tag < 1: - self.shell.log.error("The TPG Tag must be >0.") + if tag < 0: + self.shell.log.error("The TPG Tag must be 0 or more.") return tpg = TPG(self.rtsnode, tag, mode='create') if self.shell.prefs['auto_enable_tpgt']: tpg.enable = True - self.shell.log.info("Successfully created TPG %s." % tpg.tag) + self.shell.log.info("Created TPG %s." % tpg.tag) 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. + Deletes the Target Portal Group with TPG I{tag} from the target. The + I{tag} must be a positive integer matching an existing TPG. SEE ALSO ======== B{create} ''' self.assert_root() - tpg = TPG(self.rtsnode, tag, mode='lookup') + if tag.startswith("tpg"): + tag = tag[3:] + tpg = TPG(self.rtsnode, int(tag), mode='lookup') tpg.delete() - self.shell.log.info("Deleted TPGT %s." % tag) + self.shell.log.info("Deleted TPG %s." % tag) self.refresh() def ui_complete_delete(self, parameters, text, current_param): @@ -335,7 +337,7 @@ class UITPG(UIRTSLibNode): A generic TPG UI. ''' def __init__(self, tpg, parent): - name = "tpgt%d" % tpg.tag + name = "tpg%d" % tpg.tag UIRTSLibNode.__init__(self, name, tpg, parent) self.cfs_cwd = tpg.path self.refresh() @@ -349,12 +351,12 @@ class UITPG(UIRTSLibNode): def summary(self): if self.rtsnode.has_feature('nexus'): - description = "%s" % self.rtsnode.nexus + description = ("%s" % self.rtsnode.nexus, True) elif self.rtsnode.enable: - description = "enabled" + description = ("enabled", True) else: - description = "disabled" - return (description, True) + description = ("disabled", False) + return description def ui_command_enable(self): ''' @@ -366,10 +368,10 @@ class UITPG(UIRTSLibNode): ''' self.assert_root() if self.rtsnode.enable: - self.shell.log.info("The TPGT is already enabled.") + self.shell.log.info("The TPG is already enabled.") else: self.rtsnode.enable = True - self.shell.log.info("The TPGT has been enabled.") + self.shell.log.info("The TPG has been enabled.") def ui_command_disable(self): ''' @@ -382,9 +384,9 @@ class UITPG(UIRTSLibNode): self.assert_root() if self.rtsnode.enable: self.rtsnode.enable = False - self.shell.log.info("The TPGT has been disabled.") + self.shell.log.info("The TPG has been disabled.") else: - self.shell.log.info("The TPGT is already disabled.") + self.shell.log.info("The TPG is already disabled.") class UITarget(UITPG): @@ -456,10 +458,10 @@ class UINodeACLs(UINode): try: node_acl = NodeACL(self.tpg, wwn, mode="create") except RTSLibError, msg: - self.shell.log.error(msg) + self.shell.log.error(str(msg)) return else: - self.shell.log.info("Successfully created Node ACL for %s" + self.shell.log.info("Created Node ACL for %s" % node_acl.node_wwn) ui_node_acl = UINodeACL(node_acl, self) @@ -482,7 +484,7 @@ class UINodeACLs(UINode): self.assert_root() node_acl = NodeACL(self.tpg, wwn, mode='lookup') node_acl.delete() - self.shell.log.info("Successfully deleted Node ACL %s." % wwn) + self.shell.log.info("Deleted Node ACL %s." % wwn) self.refresh() def ui_complete_delete(self, parameters, text, current_param): @@ -579,6 +581,10 @@ class UINodeACL(UIRTSLibNode): self.shell.log.error("Incorrect LUN value.") return + if tpg_lun in (ml.tpg_lun.lun for ml in self.rtsnode.mapped_luns): + self.shell.log.warning( + "Warning: TPG LUN %d already mapped to this NodeACL" % tpg_lun) + mlun = MappedLUN(self.rtsnode, mapped_lun, tpg_lun, write_protect) ui_mlun = UIMappedLUN(mlun, self) self.shell.log.info("Created Mapped LUN %s." % mlun.mapped_lun) @@ -731,7 +737,7 @@ class UILUNs(UINode): return lun_object = LUN(self.tpg, lun, storage_object) - self.shell.log.info("Successfully created LUN %s." % lun_object.lun) + self.shell.log.info("Created LUN %s." % lun_object.lun) ui_lun = UILUN(lun_object, self) if add_mapped_luns: @@ -795,11 +801,15 @@ class UILUNs(UINode): B{create} ''' self.assert_root() - if lun.startswith('lun'): + if lun.lower().startswith("lun"): lun = lun[3:] - lun_object = LUN(self.tpg, lun) + try: + lun = int(lun) + lun_object = LUN(self.tpg, lun) + except: + raise RTSLibError("Invalid LUN") lun_object.delete() - self.shell.log.info("Successfully deleted LUN %s." % lun) + self.shell.log.info("Deleted LUN %s." % lun) # Refresh the TPG as we need to also refresh acls MappedLUNs self.parent.refresh() @@ -898,6 +908,12 @@ class UIPortals(UINode): B{delete} ''' self.assert_root() + try: + listen_all = int(ip_address.replace(".", "")) == 0 + except: + listen_all = False + if listen_all: + ip_address = "0.0.0.0" if ip_port is None: # FIXME: Add a specfile parameter to determine that ip_port = 3260 @@ -912,7 +928,7 @@ class UIPortals(UINode): self.shell.log.error("Cannot find a usable IP address to " + "create the Network Portal.") return - elif ip_address not in utils.list_eth_ips(): + elif ip_address not in utils.list_eth_ips() and not listen_all: self.shell.log.error("IP address does not exist: %s" % ip_address) return @@ -922,9 +938,8 @@ class UIPortals(UINode): self.shell.log.error("The ip_port must be an integer value.") return - portal = NetworkPortal(self.tpg, ip_address, - ip_port, mode='create') - self.shell.log.info("Successfully created network portal %s:%d." + portal = NetworkPortal(self.tpg, 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)
Locations
Projects
Search
Status Monitor
Help
OpenBuildService.org
Documentation
API Documentation
Code of Conduct
Contact
Support
@OBShq
Terms
openSUSE Build Service is sponsored by
The Open Build Service is an
openSUSE project
.
Sign Up
Log In
Places
Places
All Projects
Status Monitor