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)
openSUSE Build Service is sponsored by