File targetcli-git-update.patch of Package targetcli
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)