summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorChristophe Vu-Brugier <[email protected]>2016-09-22 22:29:14 +0200
committergit-ubuntu importer <[email protected]>2016-10-07 04:27:07 +0000
commit37b362126fa68ea2eebeb1bf0c141d4764c234ee (patch)
tree19b9a8c549377eacafe20f4ddf3540fc3d7943cb
Imported using git-ubuntu import.
Notes
Notes:
-rw-r--r--.gitignore24
-rw-r--r--COPYING176
-rw-r--r--Makefile117
-rw-r--r--README.md41
-rw-r--r--configshell/__init__.py27
-rw-r--r--configshell/console.py425
-rw-r--r--configshell/log.py171
-rw-r--r--configshell/node.py1865
-rw-r--r--configshell/prefs.py149
-rw-r--r--configshell/shell.py907
l---------configshell_fb1
-rw-r--r--debian/changelog6
-rw-r--r--debian/compat1
-rw-r--r--debian/control82
-rw-r--r--debian/copyright38
-rw-r--r--debian/gbp.conf12
-rw-r--r--debian/python-configshell-fb-doc.doc-base9
-rw-r--r--debian/python-configshell-fb.install1
-rw-r--r--debian/python3-configshell-fb.install1
-rwxr-xr-xdebian/rules18
-rw-r--r--debian/source/format1
-rw-r--r--debian/watch3
-rwxr-xr-xexamples/myshell173
-rw-r--r--rpm/python-configshell.spec.tmpl44
-rwxr-xr-xsetup.py35
25 files changed, 4327 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..93e9303
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,24 @@
+debian/changelog
+dpkg-buildpackage.log
+dpkg-buildpackage.version
+*.swp
+*.swo
+build-stamp
+build/*
+debian/files
+debian/python-configshell.debhelper.log
+debian/python-configshell.substvars
+debian/python-configshell/
+debian/configshell-doc.debhelper.log
+debian/configshell-doc.substvars
+debian/configshell-doc/
+debian/tmp/
+dist/*
+doc/*
+*.pyc
+debian/python-configshell.substvars
+debian/configshell-doc.debhelper.log
+debian/tmp/
+*.spec
+*.pyc
+rtslib-*
diff --git a/COPYING b/COPYING
new file mode 100644
index 0000000..68c771a
--- /dev/null
+++ b/COPYING
@@ -0,0 +1,176 @@
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..7f098b9
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,117 @@
+PKGNAME = configshell-fb
+NAME = configshell
+GIT_BRANCH = $$(git branch | grep \* | tr -d \*)
+VERSION = $$(basename $$(git describe --tags | tr - . | sed 's/^v//'))
+
+all:
+ @echo "Usage:"
+ @echo
+ @echo " make deb - Builds debian packages."
+ @echo " make rpm - Builds rpm packages."
+ @echo " make release - Generates the release tarball."
+ @echo
+ @echo " make clean - Cleanup the local repository build files."
+ @echo " make cleanall - Also remove dist/*"
+
+clean:
+ @rm -fv ${NAME}/*.pyc ${NAME}/*.html
+ @rm -frv doc
+ @rm -frv ${NAME}.egg-info MANIFEST build
+ @rm -frv debian/tmp
+ @rm -fv build-stamp
+ @rm -fv dpkg-buildpackage.log dpkg-buildpackage.version
+ @rm -frv *.rpm
+ @rm -fv debian/files debian/*.log debian/*.substvars
+ @rm -frv debian/${PKGNAME}-doc/ debian/python2.5-${PKGNAME}/
+ @rm -frv debian/python2.6-${PKGNAME}/ debian/python-${PKGNAME}/
+ @rm -frv results
+ @rm -fv rpm/*.spec *.spec rpm/sed* sed*
+ @rm -frv ${PKGNAME}-*
+ @echo "Finished cleanup."
+
+cleanall: clean
+ @rm -frv dist
+
+release: build/release-stamp
+build/release-stamp:
+ @mkdir -p build
+ @echo "Exporting the repository files..."
+ @git archive ${GIT_BRANCH} --prefix ${PKGNAME}-${VERSION}/ \
+ | (cd build; tar xfp -)
+ @echo "Cleaning up the target tree..."
+ @rm -f build/${PKGNAME}-${VERSION}/Makefile
+ @rm -f build/${PKGNAME}-${VERSION}/.gitignore
+ @echo "Fixing version string..."
+ @sed -i "s/__version__ = .*/__version__ = '${VERSION}'/g" \
+ build/${PKGNAME}-${VERSION}/${NAME}/__init__.py
+ @echo "Generating rpm specfile from template..."
+ @cd build/${PKGNAME}-${VERSION}; \
+ for spectmpl in rpm/*.spec.tmpl; do \
+ sed -i "s/Version:\( *\).*/Version:\1${VERSION}/g" $${spectmpl}; \
+ mv $${spectmpl} $$(basename $${spectmpl} .tmpl); \
+ done; \
+ rm -r rpm
+ @echo "Generating rpm changelog..."
+ @( \
+ version=${VERSION}; \
+ author=$$(git show HEAD --format="format:%an <%ae>" -s); \
+ date=$$(git show HEAD --format="format:%ad" -s \
+ | awk '{print $$1,$$2,$$3,$$5}'); \
+ hash=$$(git show HEAD --format="format:%H" -s); \
+ echo '* '"$${date} $${author} $${version}-1"; \
+ echo " - Generated from git commit $${hash}."; \
+ ) >> $$(ls build/${PKGNAME}-${VERSION}/*.spec)
+ @echo "Generating debian changelog..."
+ @( \
+ version=${VERSION}; \
+ author=$$(git show HEAD --format="format:%an <%ae>" -s); \
+ date=$$(git show HEAD --format="format:%aD" -s); \
+ day=$$(git show HEAD --format='format:%ai' -s \
+ | awk '{print $$1}' \
+ | awk -F '-' '{print $$3}' | sed 's/^0/ /g'); \
+ date=$$(echo $${date} \
+ | awk '{print $$1, "'"$${day}"'", $$3, $$4, $$5, $$6}'); \
+ hash=$$(git show HEAD --format="format:%H" -s); \
+ echo "${PKGNAME} ($${version}) unstable; urgency=low"; \
+ echo; \
+ echo " * Generated from git commit $${hash}."; \
+ echo; \
+ echo " -- $${author} $${date}"; \
+ echo; \
+ ) > build/${PKGNAME}-${VERSION}/debian/changelog
+ @find build/${PKGNAME}-${VERSION}/ -exec \
+ touch -t $$(date -d @$$(git show -s --format="format:%at") \
+ +"%Y%m%d%H%M.%S") {} \;
+ @mkdir -p dist
+ @cd build; tar -c --owner=0 --group=0 --numeric-owner \
+ --format=gnu -b20 --quoting-style=escape \
+ -f ../dist/${PKGNAME}-${VERSION}.tar \
+ $$(find ${PKGNAME}-${VERSION} -type f | sort)
+ @gzip -6 -n dist/${PKGNAME}-${VERSION}.tar
+ @echo "Generated release tarball:"
+ @echo " $$(ls dist/${PKGNAME}-${VERSION}.tar.gz)"
+ @touch build/release-stamp
+
+deb: release build/deb-stamp
+build/deb-stamp:
+ @echo "Building debian packages..."
+ @cd build/${PKGNAME}-${VERSION}; \
+ dpkg-buildpackage -rfakeroot -us -uc
+ @mv build/*_${VERSION}_*.deb dist/
+ @echo "Generated debian packages:"
+ @for pkg in $$(ls dist/*_${VERSION}_*.deb); do echo " $${pkg}"; done
+ @touch build/deb-stamp
+
+rpm: release build/rpm-stamp
+build/rpm-stamp:
+ @echo "Building rpm packages..."
+ @mkdir -p build/rpm
+ @build=$$(pwd)/build/rpm; dist=$$(pwd)/dist/; rpmbuild \
+ --define "_topdir $${build}" --define "_sourcedir $${dist}" \
+ --define "_rpmdir $${build}" --define "_buildir $${build}" \
+ --define "_srcrpmdir $${build}" -ba build/${PKGNAME}-${VERSION}/*.spec
+ @mv build/rpm/*-${VERSION}*.src.rpm dist/
+ @mv build/rpm/*/*-${VERSION}*.rpm dist/
+ @echo "Generated rpm packages:"
+ @for pkg in $$(ls dist/*-${VERSION}*.rpm); do echo " $${pkg}"; done
+ @touch build/rpm-stamp
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..459f5f3
--- /dev/null
+++ b/README.md
@@ -0,0 +1,41 @@
+configshell-fb
+==============
+
+A Python library for building configuration shells
+--------------------------------------------------
+configshell-fb is a Python library that provides a framework
+for building simple but nice CLI-based applications.
+
+This runs with Python 2 and 2to3 is run by setup.py to run on Python 3.
+
+configshell-fb development
+--------------------------
+configshell-fb is licensed under the Apache 2.0 license. Contributions are welcome.
+
+Since configshell-fb is used most often with targetcli-fb, the
+targetcli-fb mailing should be used for configshell-fb discussion.
+
+ * Mailing list: [targetcli-fb-devel](https://lists.fedorahosted.org/mailman/listinfo/targetcli-fb-devel)
+ * Source repo: [GitHub](https://github.com/agrover/configshell-fb)
+ * Bugs: [GitHub](https://github.com/agrover/configshell-fb/issues) or [Trac](https://fedorahosted.org/targetcli-fb/)
+ * Tarballs: [fedorahosted](https://fedorahosted.org/releases/t/a/targetcli-fb/)
+
+In-repo packaging
+-----------------
+Packaging scripts for RPM and DEB are included, but these are to make end-user
+custom packaging easier -- distributions tend to maintain their own packaging
+scripts separately. If you run into issues with packaging, start with opening
+a bug on your distro's bug reporting system.
+
+Some people do use these scripts, so we want to keep them around. Fixes for
+any breakage you encounter are welcome.
+
+"fb" -- "free branch"
+---------------------
+
+configshell-fb is a fork of the "configshell" code written by
+RisingTide Systems. The "-fb" differentiates between the original and
+this version. Please ensure to use either all "fb" versions of the
+targetcli components -- targetcli, rtslib, and configshell, or stick
+with all non-fb versions, since they are no longer strictly
+compatible.
diff --git a/configshell/__init__.py b/configshell/__init__.py
new file mode 100644
index 0000000..8580669
--- /dev/null
+++ b/configshell/__init__.py
@@ -0,0 +1,27 @@
+'''
+This file is part of ConfigShell.
+Copyright (c) 2011-2013 by Datera, Inc
+
+Licensed under the Apache License, Version 2.0 (the "License"); you may
+not use this file except in compliance with the License. You may obtain
+a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+License for the specific language governing permissions and limitations
+under the License.
+'''
+
+if __name__ == "configshell":
+ from warnings import warn
+ warn("'configshell' package name for configshell-fb is deprecated, please"
+ + " instead import 'configshell_fb'", UserWarning, stacklevel=2)
+
+from .console import Console
+from .log import Log
+from .node import ConfigNode, ExecutionError
+from .prefs import Prefs
+from .shell import ConfigShell
diff --git a/configshell/console.py b/configshell/console.py
new file mode 100644
index 0000000..8ed6b50
--- /dev/null
+++ b/configshell/console.py
@@ -0,0 +1,425 @@
+'''
+This file is part of ConfigShell.
+Copyright (c) 2011-2013 by Datera, Inc
+
+Licensed under the Apache License, Version 2.0 (the "License"); you may
+not use this file except in compliance with the License. You may obtain
+a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+License for the specific language governing permissions and limitations
+under the License.
+'''
+
+from fcntl import ioctl
+import re
+import six
+import struct
+import sys
+from termios import TIOCGWINSZ, TCSADRAIN, tcsetattr, tcgetattr
+import textwrap
+import tty
+
+from .prefs import Prefs
+
+# avoid requiring epydoc at runtime
+try:
+ import epydoc.markup.epytext
+except ImportError:
+ pass
+
+class Console(object):
+ '''
+ Implements various utility methods providing a console UI support toolkit,
+ most notably an epytext-to-console text renderer using ANSI escape
+ sequences. It uses the Borg pattern to share state between instances.
+ '''
+ _max_width = 132
+ _escape = '\033['
+ _ansi_format = _escape + '%dm%s'
+ _ansi_reset = _escape + '0m'
+ _re_ansi_seq = re.compile('(\033\[..?m)')
+
+ _ansi_styles = {'bold': 1,
+ 'underline': 4,
+ 'blink': 5,
+ 'reverse': 7,
+ 'concealed': 8}
+
+ colors = ['black', 'red', 'green', 'yellow',
+ 'blue', 'magenta', 'cyan', 'white']
+
+ _ansi_fgcolors = dict(zip(colors, range(30, 38)))
+ _ansi_bgcolors = dict(zip(colors, range(40, 48)))
+
+ __borg_state = {}
+
+ def __init__(self, stdin=sys.stdin, stdout=sys.stdout, stderr=sys.stderr):
+ '''
+ Initializes a Console instance.
+ @param stdin: The console standard input.
+ @type stdin: file object
+ @param stdout: The console standard output.
+ @type stdout: file object
+ '''
+ self.__dict__ = self.__borg_state
+ self._stdout = stdout
+ self._stdin = stdin
+ self._stderr = stderr
+ self.prefs = Prefs()
+
+ # Public methods
+
+ def escape(self, sequence, reply_terminator=None):
+ '''
+ Sends an escape sequence to the console, and reads the reply terminated
+ by reply_terminator. If reply_terminator is not specified, the reply
+ will not be read.
+ @type sequence: str
+ @param reply_terminator: The expected end-of-reply marker.
+ @type reply_terminator: str
+ '''
+ attributes = tcgetattr(self._stdin)
+ tty.setraw(self._stdin)
+ try:
+ self.raw_write(self._escape + sequence)
+ if reply_terminator is not None:
+ reply = ''
+ while reply[-len(reply_terminator):] != reply_terminator:
+ reply += self._stdin.read(1)
+ finally:
+ tcsetattr(self._stdin, TCSADRAIN, attributes)
+ if reply_terminator is not None:
+ reply = reply[:-len(reply_terminator)]
+ reply = reply.replace(self._escape, '').split(';')
+ return reply
+
+ def get_width(self):
+ '''
+ Returns the console width, or maximum width if we are not a terminal
+ device.
+ '''
+ try:
+ winsize = struct.pack("HHHH", 0, 0, 0, 0)
+ winsize = ioctl(self._stdout.fileno(), TIOCGWINSZ, winsize)
+ width = struct.unpack("HHHH", winsize)[1]
+ except IOError:
+ width = self._max_width
+ else:
+ if width > self._max_width:
+ width = self._max_width
+
+ return width
+
+ def get_cursor_xy(self):
+ '''
+ Get the current text cursor x, y coordinates.
+ '''
+ coords = [int(coord) for coord in self.escape("6n", "R")]
+ coords.reverse()
+ return coords
+
+ def set_cursor_xy(self, xpos, ypos):
+ '''
+ Set the cursor x, y coordinates.
+ @param xpos: The x coordinate of the cursor.
+ @type xpos: int
+ @param ypos: The y coordinate of the cursor.
+ @type ypos: int
+ '''
+ self.escape("%d;%dH" % (ypos, xpos))
+
+ def raw_write(self, text, output=sys.stdout):
+ '''
+ Raw console printing function.
+ @param text: The text to print.
+ @type text: str
+ '''
+ output.write(text)
+ output.flush()
+
+ def display(self, text, no_lf=False, error=False):
+ '''
+ Display a text with a default style.
+ @param text: Text to display
+ @type text: str
+ @param no_lf: Do not display a line feed.
+ @type no_lf: bool
+ '''
+ text = self.render_text(text)
+
+ if error:
+ output = self._stderr
+ else:
+ output = self._stdout
+
+ self.raw_write(text, output=output)
+ if not no_lf:
+ self.raw_write('\n', output=output)
+
+ def epy_write(self, text):
+ '''
+ Renders and print and epytext-formatted text on the console.
+ '''
+ text = self.dedent(text)
+ try:
+ dom_tree = epydoc.markup.epytext.parse(text, None)
+ except NameError:
+ # epydoc not installed, strip markup
+ dom_tree = text
+ dom_tree = dom_tree.replace("B{", "")
+ dom_tree = dom_tree.replace("I{", "")
+ dom_tree = dom_tree.replace("C{", "")
+ dom_tree = dom_tree.replace("}", "")
+ dom_tree += "\n"
+ except:
+ self.display(text)
+ raise
+ text = self.render_domtree(dom_tree)
+ # We need to remove the last line feed, but there might be
+ # escape characters after it...
+ clean_text = ''
+ for index in range(1, len(text)):
+ if text[-index] == '\n':
+ clean_text = text[:-index]
+ if index != 1:
+ clean_text += text[-index+1:]
+ break
+ else:
+ clean_text = text
+ self.raw_write(clean_text)
+
+ def indent(self, text, margin=2):
+ '''
+ Indents text by margin space.
+ @param text: The text to be indented.
+ @type text: str
+ '''
+ output = ''
+ for line in text.split('\n'):
+ output += margin * ' ' + line + '\n'
+ return output
+
+ def dedent(self, text):
+ '''
+ A convenience function to easily write multiline text blocks that
+ will be later assembled in to a unique epytext string.
+ It removes heading newline chars and common indentation.
+ '''
+ for i in range(len(text)):
+ if text[i] != '\n':
+ break
+ text = text[i:]
+ text = textwrap.dedent(text)
+ text = '\n' * i + text
+
+ return text
+
+ def render_text(self, text, fgcolor=None, bgcolor=None, styles=None,
+ open_end=False, todefault=False):
+ '''
+ Renders some text with ANSI console colors and attributes.
+ @param fgcolor: ANSI color to use for text:
+ black, red, green, yellow, blue, magenta. cyan. white
+ @type fgcolor: str
+ @param bgcolor: ANSI color to use for background:
+ black, red, green, yellow, blue, magenta. cyan. white
+ @type bgcolor: str
+ @param styles: List of ANSI styles to use:
+ bold, underline, blink, reverse, concealed
+ @type styles: list of str
+ @param open_end: Do not reset text style at the end ot the output.
+ @type open_end: bool
+ @param todefault: Instead of resetting style at the end of the
+ output, reset to default color. Only if not open_end.
+ @type todefault: bool
+ '''
+ if self.prefs['color_mode'] and self._stdout.isatty():
+ if fgcolor is None:
+ if self.prefs['color_default']:
+ fgcolor = self.prefs['color_default']
+ if fgcolor is not None:
+ text = self._ansi_format % (self._ansi_fgcolors[fgcolor], text)
+ if bgcolor is not None:
+ text = self._ansi_format % (self._ansi_bgcolors[bgcolor], text)
+ if styles is not None:
+ for style in styles:
+ text = self._ansi_format % (self._ansi_styles[style], text)
+ if not open_end:
+ text += self._ansi_reset
+ if todefault and fgcolor is not None:
+ if self.prefs['color_default']:
+ text += self._ansi_format \
+ % (self._ansi_fgcolors[
+ self.prefs['color_default']], '')
+ return text
+
+ def wordwrap(self, text, indent=0, startindex=0, splitchars=''):
+ '''
+ Word-wrap the given string. I.e., add newlines to the string such
+ that any lines that are longer than terminal width or max_width
+ are broken into shorter lines (at the first whitespace sequence that
+ occurs before the limit. If the given string contains newlines, they
+ will I{not} be removed. Any lines that begin with whitespace will not
+ be wordwrapped.
+
+ This version takes into account ANSI escape characters:
+ - stop escape sequence styling at the end of a split line
+ - start it again on the next line if needed after the indent
+ - do not account for the length of the escape sequences when
+ wrapping
+
+ @param indent: If specified, then indent each line by this number
+ of spaces.
+ @type indent: C{int}
+ @param startindex: If specified, then assume that the first line
+ is already preceeded by C{startindex} characters.
+ @type startindex: C{int}
+ @param splitchars: A list of non-whitespace characters which can
+ be used to split a line. (E.g., use '/\\' to allow path names
+ to be split over multiple lines.)
+ @rtype: C{str}
+ '''
+ right = self.get_width()
+ if splitchars:
+ chunks = re.split(r'( +|\n|[^ \n%s]*[%s])' %
+ (re.escape(splitchars), re.escape(splitchars)),
+ text.expandtabs())
+ else:
+ chunks = re.split(r'( +|\n)', text.expandtabs())
+ result = [' '*(indent-startindex)]
+ charindex = max(indent, startindex)
+ current_style = ''
+ for chunknum, chunk in enumerate(chunks):
+ chunk_groups = re.split(self._re_ansi_seq, chunk)
+ chunk_text = ''
+ next_style = current_style
+
+ for group in chunk_groups:
+ if re.match(self._re_ansi_seq, group) is None:
+ chunk_text += group
+ else:
+ next_style += group
+
+ chunk_len = len(chunk_text)
+ if (charindex + chunk_len > right and charindex > 0) \
+ or chunk == '\n':
+ result[-1] = result[-1].rstrip()
+ result.append(self.render_text(
+ '\n' + ' '*indent + current_style, open_end=True))
+ charindex = indent
+ if chunk[:1] not in ('\n', ' '):
+ result.append(chunk)
+ charindex += chunk_len
+ else:
+ result.append(chunk)
+ charindex += chunk_len
+
+ current_style = next_style.split(self._ansi_reset)[-1]
+
+ return ''.join(result).rstrip()+'\n'
+
+ def render_domtree(self, tree, indent=0, seclevel=0):
+ '''
+ Convert a DOM document encoding epytext to an 8-bits ascii string with
+ ANSI formating for simpler styles.
+
+ @param tree: A DOM document encoding of an epytext string.
+ @type tree: C{Element}
+ @param indent: The indentation for the string representation of
+ C{tree}. Each line of the returned string will begin with
+ C{indent} space characters.
+ @type indent: C{int}
+ @param seclevel: The section level that C{tree} appears at. This
+ is used to generate section headings.
+ @type seclevel: C{int}
+ @return: The formated string.
+ @rtype: C{string}
+ '''
+ if isinstance(tree, six.string_types):
+ return tree
+
+ if tree.tag == 'section':
+ seclevel += 1
+
+ # Figure out the child indent level.
+ if tree.tag == 'epytext':
+ cindent = indent
+ elif tree.tag == 'li' and tree.attribs.get('bullet'):
+ cindent = indent + 1 + len(tree.attribs.get('bullet'))
+ else:
+ cindent = indent + 2
+
+ variables = [self.render_domtree(c, cindent, seclevel)
+ for c in tree.children]
+ childstr = ''.join(variables)
+
+ if tree.tag == 'para':
+ text = self.render_text(childstr)
+ text = self.wordwrap(text, indent)+'\n'
+ elif tree.tag == 'li':
+ # We should be able to use getAttribute here; but there's no
+ # convenient way to test if an element has an attribute..
+ bullet = tree.attribs.get('bullet') or '-'
+ text = indent*' ' + bullet + ' ' + childstr.lstrip()
+ elif tree.tag == 'heading':
+ text = ((indent-2)*' ' + self.render_text(
+ childstr, styles=['bold'], todefault=True) \
+ + '\n')
+ elif tree.tag == 'doctestblock':
+ lines = [(indent+2)*' '+line for line in childstr.split('\n')]
+ text = '\n'.join(lines) + '\n\n'
+ elif tree.tag == 'literalblock':
+ lines = [(indent+1)*' '+ self.render_text(
+ line, todefault=True)
+ for line in childstr.split('\n')]
+ text = '\n'.join(lines) + '\n\n'
+ elif tree.tag == 'fieldlist':
+ text = childstr
+ elif tree.tag == 'field':
+ numargs = 0
+ while tree.children[numargs+1].tag == 'arg':
+ numargs += 1
+ args = variables[1:1+numargs]
+ body = variables[1+numargs:]
+ text = (indent)*' '+'@'+variables[0]
+ if args:
+ text += '(' + ', '.join(args) + ')'
+ text = text + ':\n' + ''.join(body)
+ elif tree.tag == 'uri':
+ if len(variables) != 2:
+ raise ValueError('Bad URI ')
+ elif variables[0] == variables[1]:
+ text = self.render_text(
+ '%s' % variables[1],
+ 'blue', styles=['underline'], todefault=True)
+ else:
+ text = '%r<%s>' % (variables[0], variables[1])
+ elif tree.tag == 'link':
+ if len(variables) != 2:
+ raise ValueError('Bad Link')
+ text = '%s' % variables[0]
+ elif tree.tag in ('olist', 'ulist'):
+ text = childstr.replace('\n\n', '\n')+'\n'
+ elif tree.tag == 'bold':
+ text = self.render_text(
+ childstr, styles=['bold'], todefault=True)
+ elif tree.tag == 'italic':
+ text = self.render_text(
+ childstr, styles=['underline'], todefault=True)
+ elif tree.tag == 'symbol':
+ text = '%s' \
+ % epydoc.markup.epytext.SYMBOL_TO_PLAINTEXT.get(
+ childstr, childstr)
+ elif tree.tag == 'graph':
+ text = '<<%s graph: %s>>' \
+ % (variables[0], ', '.join(variables[1:]))
+ else:
+ # Assume that anything else can be passed through.
+ text = self.render_text(childstr)
+
+ return text
diff --git a/configshell/log.py b/configshell/log.py
new file mode 100644
index 0000000..e932aaa
--- /dev/null
+++ b/configshell/log.py
@@ -0,0 +1,171 @@
+'''
+This file is part of ConfigShell.
+Copyright (c) 2011-2013 by Datera, Inc
+
+Licensed under the Apache License, Version 2.0 (the "License"); you may
+not use this file except in compliance with the License. You may obtain
+a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+License for the specific language governing permissions and limitations
+under the License.
+'''
+
+import inspect
+import os
+import time
+import traceback
+
+from .console import Console
+from .prefs import Prefs
+
+class Log(object):
+ '''
+ Implements a file and console logger using python's logging facility.
+ Log levels are, in raising criticality:
+ - debug
+ - info
+ - warning
+ - error
+ - critical
+ It uses configshell's Prefs() backend for storing some of its parameters,
+ who can then be read/changed by other objects using Prefs()
+ '''
+ __borg_state = {}
+ levels = ['critical', 'error', 'warning', 'info', 'debug']
+ colors = {'critical': 'red', 'error': 'red', 'warning': 'yellow',
+ 'info': 'green', 'debug': 'blue'}
+
+ def __init__(self, console_level=None,
+ logfile=None, file_level=None):
+ '''
+ This class implements the Borg pattern.
+ @param console_level: Console log level, defaults to 'info'
+ @type console_level: str
+ @param logfile: Optional logfile.
+ @type logfile: str
+ @param file_level: File log level, defaults to 'debug'.
+ @type file_level: str
+ '''
+ self.__dict__ = self.__borg_state
+ self.con = Console()
+ self.prefs = Prefs()
+
+ if console_level:
+ self.prefs['loglevel_console'] = console_level
+ elif not self.prefs['loglevel_console']:
+ self.prefs['loglevel_console'] = 'info'
+
+ if file_level:
+ self.prefs['loglevel_file'] = file_level
+ elif not self.prefs['loglevel_file']:
+ self.prefs['loglevel_file'] = 'debug'
+
+ if logfile:
+ self.prefs['logfile'] = logfile
+
+ # Private methods
+
+ def _append(self, msg, level):
+ '''
+ Just appends the message to the logfile if it exists, prefixing it with
+ the current time and level.
+ @param msg: The message to log
+ @type msg: str
+ @param level: The debug level to prefix the message with.
+ @type level: str
+ '''
+ date_fields = time.localtime()
+ date = "%d-%02d-%02d %02d:%02d:%02d" \
+ % (date_fields[0], date_fields[1], date_fields[2],
+ date_fields[3], date_fields[4], date_fields[5])
+
+ if self.prefs['logfile']:
+ path = os.path.expanduser(self.prefs['logfile'])
+ handle = open(path, 'a')
+ try:
+ handle.write("[%s] %s %s\n" % (level, date, msg))
+ finally:
+ handle.close()
+
+ def _log(self, level, msg):
+ '''
+ Do the actual logging.
+ @param level: The log level of the message.
+ @type level: str
+ @param msg: The message to log.
+ @type msg: str
+ '''
+ if self.levels.index(self.prefs['loglevel_file']) \
+ >= self.levels.index(level):
+ self._append(msg, level.upper())
+
+ if self.levels.index(self.prefs['loglevel_console']) \
+ >= self.levels.index(level):
+ if self.prefs["color_mode"]:
+ msg = self.con.render_text(msg, self.colors[level])
+ else:
+ msg = "%s: %s" % (level.capitalize(), msg)
+ error = False
+ if self.levels.index(level) <= self.levels.index('error'):
+ error = True
+ self.con.display(msg, error=error)
+
+ # Public methods
+
+ def debug(self, msg):
+ '''
+ Logs a debug message.
+ @param msg: The message to log.
+ @type msg: str
+ '''
+ caller = inspect.stack()[1]
+ msg = "%s:%d %s() %s" % (caller[1], caller[2], caller[3], msg)
+ self._log('debug', msg)
+
+ def exception(self, msg=None):
+ '''
+ Logs an error message and dumps a full stack trace.
+ @param msg: The message to log.
+ @type msg: str
+ '''
+ trace = traceback.format_exc().rstrip()
+ if msg:
+ trace += '\n%s' % msg
+ self._log('error', trace)
+
+ def info(self, msg):
+ '''
+ Logs an info message.
+ @param msg: The message to log.
+ @type msg: str
+ '''
+ self._log('info', msg)
+
+ def warning(self, msg):
+ '''
+ Logs a warning message.
+ @param msg: The message to log.
+ @type msg: str
+ '''
+ self._log('warning', msg)
+
+ def error(self, msg):
+ '''
+ Logs an error message.
+ @param msg: The message to log.
+ @type msg: str
+ '''
+ self._log('error', msg)
+
+ def critical(self, msg):
+ '''
+ Logs a critical message.
+ @param msg: The message to log.
+ @type msg: str
+ '''
+ self._log('critical', msg)
diff --git a/configshell/node.py b/configshell/node.py
new file mode 100644
index 0000000..848c667
--- /dev/null
+++ b/configshell/node.py
@@ -0,0 +1,1865 @@
+'''
+This file is part of ConfigShell.
+Copyright (c) 2011-2013 by Datera, Inc
+
+Licensed under the Apache License, Version 2.0 (the "License"); you may
+not use this file except in compliance with the License. You may obtain
+a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+License for the specific language governing permissions and limitations
+under the License.
+'''
+
+import inspect
+import re
+import six
+
+class ExecutionError(Exception):
+ pass
+
+class ConfigNode(object):
+ '''
+ The ConfigNode class defines a common skeleton to be used by specific
+ implementation. It is "purely virtual" (sorry for using non-pythonic
+ vocabulary there ;-) ).
+ '''
+ _path_separator = '/'
+ _path_current = '.'
+ _path_previous = '..'
+
+ ui_type_method_prefix = "ui_type_"
+ ui_command_method_prefix = "ui_command_"
+ ui_complete_method_prefix = "ui_complete_"
+ ui_setgroup_method_prefix = "ui_setgroup_"
+ ui_getgroup_method_prefix = "ui_getgroup_"
+
+ help_intro = '''
+ GENERALITIES
+ ============
+ This is a shell in which you can create, delete and configure
+ configuration objects.
+
+ The available commands depend on the current path or target
+ path you want to run a command in: different path have
+ different sets of available commands, i.e. a path pointing at
+ an iscsi target will not have the same availaible commands as,
+ say, a path pointing at a storage object.
+
+ The prompt that starts each command line indicates your
+ current path. Alternatively (useful if the prompt displays
+ an abbreviated path to save space), you can run the
+ B{pwd} command to display the complete current path.
+
+ Navigating the tree is done using the B{cd} command. Without
+ any argument, B{cd} will present you wil the full objects
+ tree. Just use arrows to select the destination path, and
+ enter will get you there. Please try B{help cd} for navigation
+ tips.
+
+ COMMAND SYNTAX
+ ==============
+ Commands are built using the following syntax:
+
+ [I{TARGET_PATH}] B{COMMAND_NAME} [I{OPTIONS}]
+
+ The I{TARGET_PATH} indicates the path to run the command from.
+ If ommited, the command will be run from your current path.
+
+ The I{OPTIONS} depend on the command. Please use B{help
+ COMMAND} to get more information.
+ '''
+
+ def __init__(self, name, parent=None, shell=None):
+ '''
+ @param parent: The parent ConfigNode of the new object. If None, then
+ the ConfigNode will be a root node.
+ @type parent: ConfigNode or None
+ @param shell: The shell to attach a root node to.
+ @type shell: ConfigShell
+ '''
+ self._name = name
+ self._children = set([])
+ if parent is None:
+ if shell is None:
+ raise ValueError("A root ConfigNode must have a shell.")
+ else:
+ self._parent = None
+ self._shell = shell
+ shell.attach_root_node(self)
+ else:
+ if shell is None:
+ self._parent = parent
+ self._shell = None
+ else:
+ raise ValueError("A non-root ConfigNode can't have a shell.")
+
+ if self._parent is not None:
+ for sibling in self._parent._children:
+ if sibling.name == name:
+ raise ValueError("Name '%s' already used by a sibling."
+ % self._name)
+ self._parent._children.add(self)
+
+ self._configuration_groups = {}
+
+ self.define_config_group_param(
+ 'global', 'tree_round_nodes', 'bool',
+ 'Tree node display style.')
+ self.define_config_group_param(
+ 'global', 'tree_status_mode', 'bool',
+ 'Whether or not to display status in tree.')
+ self.define_config_group_param(
+ 'global', 'tree_max_depth', 'number',
+ 'Maximum depth of displayed node tree.')
+ self.define_config_group_param(
+ 'global', 'tree_show_root', 'bool',
+ 'Whether or not to display tree root.')
+ self.define_config_group_param(
+ 'global', 'color_mode', 'bool',
+ 'Console color display mode.')
+ self.define_config_group_param(
+ 'global', 'loglevel_console', 'loglevel',
+ 'Log level for messages going to the console.')
+ self.define_config_group_param(
+ 'global', 'loglevel_file', 'loglevel',
+ 'Log level for messages going to the log file.')
+ self.define_config_group_param(
+ 'global', 'logfile', 'string',
+ 'Logfile to use.')
+ self.define_config_group_param(
+ 'global', 'color_default', 'colordefault',
+ 'Default text display color.')
+ self.define_config_group_param(
+ 'global', 'color_path', 'color',
+ 'Color to use for path completions')
+ self.define_config_group_param(
+ 'global', 'color_command', 'color',
+ 'Color to use for command completions.')
+ self.define_config_group_param(
+ 'global', 'color_parameter', 'color',
+ 'Color to use for parameter completions.')
+ self.define_config_group_param(
+ 'global', 'color_keyword', 'color',
+ 'Color to use for keyword completions.')
+ self.define_config_group_param(
+ 'global', 'prompt_length', 'number',
+ 'Max length of the shell prompt path, 0 for infinite.')
+
+ if self.shell.prefs['bookmarks'] is None:
+ self.shell.prefs['bookmarks'] = {}
+
+ # User interface types
+
+ def ui_type_number(self, value=None, enum=False, reverse=False):
+ '''
+ UI parameter type helper for number parameter type.
+ @param value: Value to check against the type.
+ @type value: anything
+ @param enum: Has a meaning only if value is omitted. If set, returns
+ a list of the possible values for the type, or [] if this is not
+ possible. If not set, returns a text description of the type format.
+ @type enum: bool
+ @param reverse: If set, translates an internal value to its UI
+ string representation.
+ @type reverse: bool
+ @return: c.f. parameter enum description.
+ @rtype: str|list|None
+ @raise ValueError: If the value does not check ok against the type.
+ '''
+ if reverse:
+ if value is not None:
+ return str(value)
+ else:
+ return 'n/a'
+
+ type_enum = []
+ syntax = "NUMBER"
+ if value is None:
+ if enum:
+ return type_enum
+ else:
+ return syntax
+ elif not value:
+ return None
+ else:
+ try:
+ value = int(value)
+ except ValueError:
+ raise ValueError("Syntax error, '%s' is not a %s."
+ % (value, syntax))
+ else:
+ return value
+
+ def ui_type_string(self, value=None, enum=False, reverse=False):
+ '''
+ UI parameter type helper for string parameter type.
+ @param value: Value to check against the type.
+ @type value: anything
+ @param enum: Has a meaning only if value is omitted. If set, returns
+ a list of the possible values for the type, or [] if this is not
+ possible. If not set, returns a text description of the type format.
+ @type enum: bool
+ @param reverse: If set, translates an internal value to its UI
+ string representation.
+ @type reverse: bool
+ @return: c.f. parameter enum description.
+ @rtype: str|list|None
+ @raise ValueError: If the value does not check ok against the type.
+ '''
+ if reverse:
+ if value is not None:
+ return value
+ else:
+ return 'n/a'
+
+ type_enum = []
+ syntax = "STRING_OF_TEXT"
+ if value is None:
+ if enum:
+ return type_enum
+ else:
+ return syntax
+ elif not value:
+ return None
+ else:
+ try:
+ value = str(value)
+ except ValueError:
+ raise ValueError("Syntax error, '%s' is not a %s."
+ % (value, syntax))
+ else:
+ return value
+
+ def ui_type_bool(self, value=None, enum=False, reverse=False):
+ '''
+ UI parameter type helper for boolean parameter type. Valid values are
+ either 'true' or 'false'.
+ @param value: Value to check against the type.
+ @type value: anything
+ @param enum: Has a meaning only if value is omitted. If set, returns
+ a list of the possible values for the type, or None if this is not
+ possible. If not set, returns a text description of the type format.
+ @type enum: bool
+ @param reverse: If set, translates an internal value to its UI
+ string representation.
+ @type reverse: bool
+ @return: c.f. parameter enum description.
+ @rtype: str|list|None
+ @raise ValueError: If the value does not check ok againts the type.
+ '''
+ if reverse:
+ if value:
+ return 'true'
+ else:
+ return 'false'
+ type_enum = ['true', 'false']
+ syntax = '|'.join(type_enum)
+ if value is None:
+ if enum:
+ return type_enum
+ else:
+ return syntax
+ elif value.lower() == 'true':
+ return True
+ elif value.lower() == 'false':
+ return False
+ else:
+ raise ValueError("Syntax error, '%s' is not %s."
+ % (value, syntax))
+
+ def ui_type_loglevel(self, value=None, enum=False, reverse=False):
+ '''
+ UI parameter type helper for log level parameter type.
+ @param value: Value to check against the type.
+ @type value: anything
+ @param enum: Has a meaning only if value is omitted. If set, returns
+ a list of the possible values for the type, or None if this is not
+ possible. If not set, returns a text description of the type format.
+ @type enum: bool
+ @param reverse: If set, translates an internal value to its UI
+ string representation.
+ @type reverse: bool
+ @return: c.f. parameter enum description.
+ @rtype: str|list|None
+ @raise ValueError: If the value does not check ok againts the type.
+ '''
+ if reverse:
+ if value is not None:
+ return value
+ else:
+ return 'n/a'
+
+ type_enum = self.shell.log.levels
+ syntax = '|'.join(type_enum)
+ if value is None:
+ if enum:
+ return type_enum
+ else:
+ return syntax
+ elif value in type_enum:
+ return value
+ else:
+ raise ValueError("Syntax error, '%s' is not %s"
+ % (value, syntax))
+
+ def ui_type_color(self, value=None, enum=False, reverse=False):
+ '''
+ UI parameter type helper for color parameter type.
+ @param value: Value to check against the type.
+ @type value: anything
+ @param enum: Has a meaning only if value is omitted. If set, returns
+ a list of the possible values for the type, or None if this is not
+ possible. If not set, returns a text description of the type format.
+ @type enum: bool
+ @param reverse: If set, translates an internal value to its UI
+ string representation.
+ @type reverse: bool
+ @return: c.f. parameter enum description.
+ @rtype: str|list|None
+ @raise ValueError: If the value does not check ok againts the type.
+ '''
+ if reverse:
+ if value is not None:
+ return value
+ else:
+ return 'default'
+
+ type_enum = self.shell.con.colors + ['default']
+ syntax = '|'.join(type_enum)
+ if value is None:
+ if enum:
+ return type_enum
+ else:
+ return syntax
+ elif not value or value == 'default':
+ return None
+ elif value in type_enum:
+ return value
+ else:
+ raise ValueError("Syntax error, '%s' is not %s"
+ % (value, syntax))
+
+ def ui_type_colordefault(self, value=None, enum=False, reverse=False):
+ '''
+ UI parameter type helper for default color parameter type.
+ @param value: Value to check against the type.
+ @type value: anything
+ @param enum: Has a meaning only if value is omitted. If set, returns
+ a list of the possible values for the type, or None if this is not
+ possible. If not set, returns a text description of the type format.
+ @type enum: bool
+ @param reverse: If set, translates an internal value to its UI
+ string representation.
+ @type reverse: bool
+ @return: c.f. parameter enum description.
+ @rtype: str|list|None
+ @raise ValueError: If the value does not check ok againts the type.
+ '''
+ if reverse:
+ if value is not None:
+ return value
+ else:
+ return 'none'
+
+ type_enum = self.shell.con.colors + ['none']
+ syntax = '|'.join(type_enum)
+ if value is None:
+ if enum:
+ return type_enum
+ else:
+ return syntax
+ elif not value or value == 'none':
+ return None
+ elif value in type_enum:
+ return value
+ else:
+ raise ValueError("Syntax error, '%s' is not %s"
+ % (value, syntax))
+
+
+ # User interface get/set methods
+
+ def ui_setgroup_global(self, parameter, value):
+ '''
+ This is the backend method for setting parameters in configuration
+ group 'global'. It simply uses the Prefs() backend to store the global
+ preferences for the shell. Some of these group parameters are shared
+ using the same Prefs() object by the Log() and Console() classes, so
+ this backend should not be changed without taking this into
+ consideration.
+
+ The parameters getting to us have already been type-checked and casted
+ by the type-check methods registered in the config group via the ui set
+ command, and their existence in the group has also been checked. Thus
+ our job is minimal here. Also, it means that overhead when called with
+ generated arguments (as opposed to user-supplied) gets minimal
+ overhead, and allows setting new parameters without error.
+
+ @param parameter: The parameter to set.
+ @type parameter: str
+ @param value: The value
+ @type value: arbitrary
+ '''
+ self.shell.prefs[parameter] = value
+
+ def ui_getgroup_global(self, parameter):
+ '''
+ This is the backend method for getting configuration parameters out of
+ the B{global} configuration group. It gets the values from the Prefs()
+ backend. Eventual casting to str for UI display is handled by the ui
+ get command, for symmetry with the pendant ui_setgroup method.
+ Existence of the parameter in the group should have already been
+ checked by the ui get command, so we go blindly about this. This might
+ allow internal client code to get a None value if the parameter does
+ not exist, as supported by Prefs().
+
+ @param parameter: The parameter to get the value of.
+ @type parameter: str
+ @return: The parameter's value
+ @rtype: arbitrary
+ '''
+ return self.shell.prefs[parameter]
+
+ def ui_eval_param(self, ui_value, type, default):
+ '''
+ Evaluates a user-provided parameter value using a given type helper.
+ If the parameter value is None, the default will be returned. If the
+ ui_value does not check out with the type helper, and execution error
+ will be raised.
+
+ @param ui_value: The user provided parameter value.
+ @type ui_value: str
+ @param type: The ui_type to be used
+ @type type: str
+ @param default: The default value to return.
+ @type default: any
+ @return: The evaluated parameter value.
+ @rtype: depends on type
+ @raise ExecutionError: If evaluation fails.
+ '''
+ type_method = self.get_type_method(type)
+ if ui_value is None:
+ return default
+ else:
+ try:
+ value = type_method(ui_value)
+ except ValueError as msg:
+ raise ExecutionError(msg)
+ else:
+ return value
+
+ def get_type_method(self, type):
+ '''
+ Returns the type helper method matching the type name.
+ '''
+ return getattr(self, "%s%s" % (self.ui_type_method_prefix, type))
+
+ # User interface commands
+
+ def ui_command_set(self, group=None, **parameter):
+ '''
+ Sets one or more configuration parameters in the given group.
+ The B{global} group contains all global CLI preferences.
+ Other groups are specific to the current path.
+
+ Run with no parameter nor group to list all available groups, or
+ with just a group name to list all available parameters within that
+ group.
+
+ Example: B{set global color_mode=true loglevel_console=info}
+
+ SEE ALSO
+ ========
+ get
+ '''
+ if group is None:
+ self.shell.con.epy_write('''
+ AVAILABLE CONFIGURATION GROUPS
+ ==============================
+ %s
+ ''' % ' '.join(self.list_config_groups()))
+ elif not parameter:
+ if group not in self.list_config_groups():
+ raise ExecutionError("Unknown configuration group: %s" % group)
+
+ section = "%s CONFIG GROUP" % group.upper()
+ underline1 = ''.ljust(len(section), '=')
+ parameters = ''
+ for p_name in self.list_group_params(group, writable=True):
+ p_def = self.get_group_param(group, p_name)
+ type_method = self.get_type_method(p_def['type'])
+ p_name = "%s=I{%s}" % (p_def['name'], p_def['type'])
+ underline2 = ''.ljust(len(p_name), '-')
+ parameters += '%s\n%s\n%s\n\n' \
+ % (p_name, underline2, p_def['description'])
+ self.shell.con.epy_write('''%s\n%s\n%s\n'''
+ % (section, underline1, parameters))
+
+ elif group not in self.list_config_groups():
+ raise ExecutionError("Unknown configuration group: %s" % group)
+
+ for param, value in six.iteritems(parameter):
+ if param not in self.list_group_params(group):
+ raise ExecutionError("Unknown parameter %s in group '%s'."
+ % (param, group))
+
+ p_def = self.get_group_param(group, param)
+ type_method = self.get_type_method(p_def['type'])
+ if not p_def['writable']:
+ raise ExecutionError("Parameter %s is read-only." % param)
+
+ try:
+ value = type_method(value)
+ except ValueError as msg:
+ raise ExecutionError("Not setting %s! %s" % (param, msg))
+
+ group_setter = self.get_group_setter(group)
+ group_setter(param, value)
+ group_getter = self.get_group_getter(group)
+ value = group_getter(param)
+ value = type_method(value, reverse=True)
+ self.shell.con.display("Parameter %s is now '%s'." % (param, value))
+
+ def ui_complete_set(self, parameters, text, current_param):
+ '''
+ Parameter auto-completion method for user command set.
+ @param parameters: Parameters on the command line.
+ @type parameters: dict
+ @param text: Current text of parameter being typed by the user.
+ @type text: str
+ @param current_param: Name of parameter to complete.
+ @type current_param: str
+ @return: Possible completions
+ @rtype: list of str
+ '''
+ completions = []
+
+ self.shell.log.debug("Called with params=%s, text='%s', current='%s'"
+ % (str(parameters), text, current_param))
+
+ if current_param == 'group':
+ completions = [group for group in self.list_config_groups()
+ if group.startswith(text)]
+ elif 'group' in parameters:
+ group = parameters['group']
+ if group in self.list_config_groups():
+ group_params = self.list_group_params(group, writable=True)
+ if current_param in group_params:
+ p_def = self.get_group_param(group, current_param)
+ type_method = self.get_type_method(p_def['type'])
+ type_enum = type_method(enum=True)
+ if type_enum is not None:
+ type_enum = [item for item in type_enum
+ if item.startswith(text)]
+ completions.extend(type_enum)
+ else:
+ group_params = ([param + '=' for param in group_params
+ if param.startswith(text)
+ if param not in parameters])
+ if group_params:
+ completions.extend(group_params)
+
+ if len(completions) == 1 and not completions[0].endswith('='):
+ completions = [completions[0] + ' ']
+
+ self.shell.log.debug("Returning completions %s." % str(completions))
+ return completions
+
+ def ui_command_get(self, group=None, *parameter):
+ '''
+ Gets the value of one or more configuration parameters in the given
+ group.
+
+ Run with no parameter nor group to list all available groups, or
+ with just a group name to list all available parameters within that
+ group.
+
+ Example: B{get global color_mode loglevel_console}
+
+ SEE ALSO
+ ========
+ set
+ '''
+ if group is None:
+ self.shell.con.epy_write('''
+ AVAILABLE CONFIGURATION GROUPS
+ ==============================
+ %s
+ ''' % ' '.join(self.list_config_groups()))
+ elif not parameter:
+ if group not in self.list_config_groups():
+ raise ExecutionError("Unknown configuration group: %s" % group)
+
+ section = "%s CONFIG GROUP" % group.upper()
+ underline1 = ''.ljust(len(section), '=')
+ parameters = ''
+ params = [self.get_group_param(group, p_name)
+ for p_name in self.list_group_params(group)]
+ for p_def in params:
+ group_getter = self.get_group_getter(group)
+ value = group_getter(p_def['name'])
+ type_method = self.get_type_method(p_def['type'])
+ value = type_method(value, reverse=True)
+ param = "%s=%s" % (p_def['name'], value)
+ if p_def['writable'] is False:
+ param += " [ro]"
+ underline2 = ''.ljust(len(param), '-')
+ parameters += '%s\n%s\n%s\n\n' \
+ % (param, underline2, p_def['description'])
+
+ self.shell.con.epy_write('''%s\n%s\n%s\n'''
+ % (section, underline1, parameters))
+
+ elif group not in self.list_config_groups():
+ raise ExecutionError("Unknown configuration group: %s" % group)
+
+ for param in parameter:
+ if param not in self.list_group_params(group):
+ raise ExecutionError("No parameter '%s' in group '%s'."
+ % (param, group))
+
+ self.shell.log.debug("About to get the parameter's value.")
+ group_getter = self.get_group_getter(group)
+ value = group_getter(param)
+ p_def = self.get_group_param(group, param)
+ type_method = self.get_type_method(p_def['type'])
+ value = type_method(value, reverse=True)
+ if p_def['writable']:
+ writable = ""
+ else:
+ writable = "[ro]"
+ self.shell.con.display("%s=%s %s"
+ % (param, value, writable))
+
+ def ui_complete_get(self, parameters, text, current_param):
+ '''
+ Parameter auto-completion method for user command get.
+ @param parameters: Parameters on the command line.
+ @type parameters: dict
+ @param text: Current text of parameter being typed by the user.
+ @type text: str
+ @param current_param: Name of parameter to complete.
+ @type current_param: str
+ @return: Possible completions
+ @rtype: list of str
+ '''
+ completions = []
+
+ self.shell.log.debug("Called with params=%s, text='%s', current='%s'"
+ % (str(parameters), text, current_param))
+
+ if current_param == 'group':
+ completions = [group for group in self.list_config_groups()
+ if group.startswith(text)]
+ elif 'group' in parameters:
+ group = parameters['group']
+ if group in self.list_config_groups():
+ group_params = ([param
+ for param in self.list_group_params(group)
+ if param.startswith(text)
+ if param not in parameters])
+ if group_params:
+ completions.extend(group_params)
+
+ if len(completions) == 1 and not completions[0].endswith('='):
+ completions = [completions[0] + ' ']
+
+ self.shell.log.debug("Returning completions %s." % str(completions))
+ return completions
+
+ def ui_command_ls(self, path=None, depth=None):
+ '''
+ Display either the nodes tree relative to path or to the current node.
+
+ PARAMETERS
+ ==========
+
+ I{path}
+ -------
+ The I{path} to display the nodes tree of. Can be an absolute path, a
+ relative path or a bookmark.
+
+ I{depth}
+ --------
+ The I{depth} parameter limits the maximum depth of the tree to display.
+ If set to 0, then the complete tree will be displayed (the default).
+
+ SEE ALSO
+ ========
+ cd bookmarks
+ '''
+ try:
+ target = self.get_node(path)
+ except ValueError as msg:
+ raise ExecutionError(str(msg))
+
+ if depth is None:
+ depth = self.shell.prefs['tree_max_depth']
+ try:
+ depth = int(depth)
+ except ValueError:
+ raise ExecutionError('The tree depth must be a number.')
+
+ if depth == 0:
+ depth = None
+ tree = self._render_tree(target, depth=depth)
+ self.shell.con.display(tree)
+
+ def _render_tree(self, root, margin=None, depth=None, do_list=False):
+ '''
+ Renders an ascii representation of a tree of ConfigNodes.
+ @param root: The root node of the tree
+ @type root: ConfigNode
+ @param margin: Format of the left margin to use for children.
+ True results in a pipe, and False results in no pipe.
+ Used for recursion only.
+ @type margin: list
+ @param depth: The maximum depth of nodes to display, None means
+ infinite.
+ @type depth: None or int
+ @param do_list: Return two lists, one with each line text
+ representation, the other with the corresponding paths.
+ @type do_list: bool
+ @return: An ascii tree representation or (lines, paths).
+ @rtype: str
+ '''
+ lines = []
+ paths = []
+
+ node_length = 2
+ node_shift = 2
+ level = root.path.rstrip('/').count('/')
+ if margin is None:
+ margin = [0]
+ root_call = True
+ else:
+ root_call = False
+
+ if do_list:
+ color = None
+ elif not level % 3:
+ color = None
+ elif not (level - 1) % 3:
+ color = 'blue'
+ else:
+ color = 'magenta'
+
+ if do_list:
+ styles = None
+ elif root_call:
+ styles = ['bold', 'underline']
+ else:
+ styles = ['bold']
+
+ if do_list:
+ name = root.name
+ else:
+ name = self.shell.con.render_text(root.name, color, styles=styles)
+ name_len = len(root.name)
+
+ (description, is_healthy) = root.summary()
+ if not description:
+ if is_healthy is True:
+ description = "OK"
+ elif is_healthy is False:
+ description = "ERROR"
+ else:
+ description = "..."
+
+ description_len = len(description) + 3
+
+ if do_list:
+ summary = '['
+ else:
+ summary = self.shell.con.render_text(' [', styles=['bold'])
+
+ if is_healthy is True:
+ if do_list:
+ summary += description
+ else:
+ summary += self.shell.con.render_text(description, 'green')
+ elif is_healthy is False:
+ if do_list:
+ summary += description
+ else:
+ summary += self.shell.con.render_text(description, 'red',
+ styles=['bold'])
+ else:
+ summary += description
+
+ if do_list:
+ summary += ']'
+ else:
+ summary += self.shell.con.render_text(']', styles=['bold'])
+
+ def sorting_keys(s):
+ m = re.search(r'(.*?)(\d+$)', str(s))
+ if m:
+ return (m.group(1), int(m.group(2)))
+ else:
+ return (str(s), 0)
+
+ # Sort ending numbers numerically, so we get e.g. "lun1, lun2, lun10"
+ # instead of "lun1, lun10, lun2".
+ children = sorted(root.children, key=sorting_keys)
+ line = ""
+
+ for pipe in margin[:-1]:
+ if pipe:
+ line = line + "|".ljust(node_shift)
+ else:
+ line = line + ''.ljust(node_shift)
+
+ if self.shell.prefs['tree_round_nodes']:
+ node_char = 'o'
+ else:
+ node_char = '+'
+ line += node_char.ljust(node_length, '-')
+ line += ' '
+ margin_len = len(line)
+
+ pad = (self.shell.con.get_width() - 1
+ - description_len
+ - margin_len
+ - name_len) * '.'
+ if not do_list:
+ pad = self.shell.con.render_text(pad, color)
+
+ line += name
+ if self.shell.prefs['tree_status_mode']:
+ line += ' %s%s' % (pad, summary)
+
+ lines.append(line)
+ paths.append(root.path)
+
+ if root_call \
+ and not self.shell.prefs['tree_show_root'] \
+ and not do_list:
+ tree = ''
+ for child in children:
+ tree = tree + self._render_tree(child, [False], depth)
+ else:
+ tree = line + '\n'
+ if depth is None or depth > 0:
+ if depth is not None:
+ depth = depth - 1
+ for i in range(len(children)):
+ margin.append(i<len(children)-1)
+ if do_list:
+ new_lines, new_paths = \
+ self._render_tree(children[i], margin, depth,
+ do_list=True)
+ lines.extend(new_lines)
+ paths.extend(new_paths)
+ else:
+ tree = tree \
+ + self._render_tree(children[i], margin, depth)
+ margin.pop()
+
+ if root_call:
+ if do_list:
+ return (lines, paths)
+ else:
+ return tree[:-1]
+ else:
+ if do_list:
+ return (lines, paths)
+ else:
+ return tree
+
+
+ def ui_complete_ls(self, parameters, text, current_param):
+ '''
+ Parameter auto-completion method for user command ls.
+ @param parameters: Parameters on the command line.
+ @type parameters: dict
+ @param text: Current text of parameter being typed by the user.
+ @type text: str
+ @param current_param: Name of parameter to complete.
+ @type current_param: str
+ @return: Possible completions
+ @rtype: list of str
+ '''
+ if current_param == 'path':
+ (basedir, slash, partial_name) = text.rpartition('/')
+ basedir = basedir + slash
+ target = self.get_node(basedir)
+ names = [child.name for child in target.children]
+ completions = []
+ for name in names:
+ num_matches = 0
+ if name.startswith(partial_name):
+ num_matches += 1
+ if num_matches == 1:
+ completions.append("%s%s/" % (basedir, name))
+ else:
+ completions.append("%s%s" % (basedir, name))
+ if len(completions) == 1:
+ if not self.get_node(completions[0]).children:
+ completions[0] = completions[0].rstrip('/') + ' '
+
+ # Bookmarks
+ bookmarks = ['@' + bookmark for bookmark
+ in self.shell.prefs['bookmarks']
+ if ('@' + bookmark).startswith(text)]
+ self.shell.log.debug("Found bookmarks %s." % str(bookmarks))
+ if bookmarks:
+ completions.extend(bookmarks)
+
+ self.shell.log.debug("Completions are %s." % str(completions))
+ return completions
+
+ elif current_param == 'depth':
+ if text:
+ try:
+ int(text.strip())
+ except ValueError:
+ self.shell.log.debug("Text is not a number.")
+ return []
+ return [ text + number for number
+ in [str(num) for num in range(10)]
+ if (text + number).startswith(text)]
+
+ def ui_command_cd(self, path=None):
+ '''
+ Change current path to path.
+
+ The path is constructed just like a unix path, with B{/} as separator
+ character, B{.} for the current node, B{..} for the parent node.
+
+ Suppose the nodes tree looks like this::
+ +-/
+ +-a0 (1)
+ | +-b0 (*)
+ | +-c0
+ +-a1 (3)
+ +-b0
+ +-c0
+ +-d0 (2)
+
+ Suppose the current node is the one marked (*) at the beginning of all
+ the following examples:
+ - B{cd ..} takes you to the node marked (1)
+ - B{cd .} makes you stay in (*)
+ - B{cd /a1/b0/c0/d0} takes you to the node marked (2)
+ - B{cd ../../a1} takes you to the node marked (3)
+ - B{cd /a1} also takes you to the node marked (3)
+ - B{cd /} takes you to the root node B{/}
+ - B{cd /a0/b0/./c0/../../../a1/.} takes you to the node marked (3)
+
+ You can also navigate the path history with B{<} and B{>}:
+ - B{cd <} takes you back one step in the path history
+ - B{cd >} takes you one step forward in the path history
+
+ SEE ALSO
+ ========
+ ls cd
+ '''
+ self.shell.log.debug("Changing current node to '%s'." % path)
+
+ if self.shell.prefs['path_history'] is None:
+ self.shell.prefs['path_history'] = [self.path]
+ self.shell.prefs['path_history_index'] = 0
+
+ # Go back in history to the last existing path
+ if path == '<':
+ if self.shell.prefs['path_history_index'] == 0:
+ self.shell.log.info("Reached begining of path history.")
+ return self
+ exists = False
+ while not exists:
+ if self.shell.prefs['path_history_index'] > 0:
+ self.shell.prefs['path_history_index'] = \
+ self.shell.prefs['path_history_index'] - 1
+ index = self.shell.prefs['path_history_index']
+ path = self.shell.prefs['path_history'][index]
+ try:
+ target_node = self.get_node(path)
+ except ValueError:
+ pass
+ else:
+ exists = True
+ else:
+ path = '/'
+ self.shell.prefs['path_history_index'] = 0
+ self.shell.prefs['path_history'][0] = '/'
+ exists = True
+ self.shell.log.info('Taking you back to %s.' % path)
+ return self.get_node(path)
+
+ # Go forward in history
+ if path == '>':
+ if self.shell.prefs['path_history_index'] == \
+ len(self.shell.prefs['path_history']) - 1:
+ self.shell.log.info("Reached the end of path history.")
+ return self
+ exists = False
+ while not exists:
+ if self.shell.prefs['path_history_index'] \
+ < len(self.shell.prefs['path_history']) - 1:
+ self.shell.prefs['path_history_index'] = \
+ self.shell.prefs['path_history_index'] + 1
+ index = self.shell.prefs['path_history_index']
+ path = self.shell.prefs['path_history'][index]
+ try:
+ target_node = self.get_node(path)
+ except ValueError:
+ pass
+ else:
+ exists = True
+ else:
+ path = self.path
+ self.shell.prefs['path_history_index'] \
+ = len(self.shell.prefs['path_history'])
+ self.shell.prefs['path_history'].append(path)
+ exists = True
+ self.shell.log.info('Taking you back to %s.' % path)
+ return self.get_node(path)
+
+ # Use an urwid walker to select the path
+ if path is None:
+ lines, paths = self._render_tree(self.get_root(), do_list=True)
+ start_pos = paths.index(self.path)
+ selected = self._lines_walker(lines, start_pos=start_pos)
+ path = paths[selected]
+
+ # Normal path
+ try:
+ target_node = self.get_node(path)
+ except ValueError as msg:
+ raise ExecutionError(str(msg))
+
+ index = self.shell.prefs['path_history_index']
+ if target_node.path != self.shell.prefs['path_history'][index]:
+ # Truncate the hostory to retain current path as last one
+ self.shell.prefs['path_history'] = \
+ self.shell.prefs['path_history'][:index+1]
+ # Append the new path and update the index
+ self.shell.prefs['path_history'].append(target_node.path)
+ self.shell.prefs['path_history_index'] = index + 1
+ self.shell.log.debug("After cd, path history is: %s, index is %d"
+ % (str(self.shell.prefs['path_history']),
+ self.shell.prefs['path_history_index']))
+ return target_node
+
+ def _lines_walker(self, lines, start_pos):
+ '''
+ Using the curses urwid library, displays all lines passed as argument,
+ and after allowing selection of one line using up, down and enter keys,
+ returns its index.
+ @param lines: The lines to display and select from.
+ @type lines: list of str
+ @param start_pos: The index of the line to select initially.
+ @type start_pos: int
+ @return: the index of the selected line.
+ @rtype: int
+ '''
+ import urwid
+
+ palette = [('header', 'white', 'black'),
+ ('reveal focus', 'black', 'yellow', 'standout')]
+
+ content = urwid.SimpleListWalker(
+ [urwid.AttrMap(w, None, 'reveal focus')
+ for w in [urwid.Text(line) for line in lines]])
+
+ listbox = urwid.ListBox(content)
+ frame = urwid.Frame(listbox)
+
+ def handle_input(input, raw):
+ for key in input:
+ widget, pos = content.get_focus()
+ if key == 'up':
+ if pos > 0:
+ content.set_focus(pos-1)
+ elif key == 'down':
+ try:
+ content.set_focus(pos+1)
+ except IndexError:
+ pass
+ elif key == 'enter':
+ raise urwid.ExitMainLoop()
+
+ content.set_focus(start_pos)
+ loop = urwid.MainLoop(frame, palette, input_filter=handle_input)
+ loop.run()
+ return listbox.focus_position
+
+ def ui_complete_cd(self, parameters, text, current_param):
+ '''
+ Parameter auto-completion method for user command cd.
+ @param parameters: Parameters on the command line.
+ @type parameters: dict
+ @param text: Current text of parameter being typed by the user.
+ @type text: str
+ @param current_param: Name of parameter to complete.
+ @type current_param: str
+ @return: Possible completions
+ @rtype: list of str
+ '''
+ if current_param == 'path':
+ completions = self.ui_complete_ls(parameters, text, current_param)
+ completions.extend([nav for nav in ['<', '>']
+ if nav.startswith(text)])
+ return completions
+
+ def ui_command_help(self, topic=None):
+ '''
+ Displays the manual page for a topic, or list available topics.
+ '''
+ commands = self.list_commands()
+ if topic is None:
+ msg = self.shell.con.dedent(self.help_intro)
+ msg += self.shell.con.dedent('''
+
+ AVAILABLE COMMANDS
+ ==================
+ The following commands are available in the
+ current path:
+
+ ''')
+ for command in commands:
+ msg += " - %s\n" % self.get_command_syntax(command)[0]
+ self.shell.con.epy_write(msg)
+ return
+
+ if topic not in commands:
+ raise ExecutionError("Cannot find help topic %s." % topic)
+
+ syntax, comments, defaults = self.get_command_syntax(topic)
+ msg = self.shell.con.dedent('''
+ SYNTAX
+ ======
+ %s
+
+ ''' % syntax)
+ for comment in comments:
+ msg += comment + '\n'
+
+ if defaults:
+ msg += self.shell.con.dedent('''
+ DEFAULT VALUES
+ ==============
+ %s
+
+ ''' % defaults)
+ msg += self.shell.con.dedent('''
+ DESCRIPTION
+ ===========
+ ''')
+ msg += self.get_command_description(topic)
+ msg += "\n"
+ self.shell.con.epy_write(msg)
+
+ def ui_complete_help(self, parameters, text, current_param):
+ '''
+ Parameter auto-completion method for user command help.
+ @param parameters: Parameters on the command line.
+ @type parameters: dict
+ @param text: Current text of parameter being typed by the user.
+ @type text: str
+ @param current_param: Name of parameter to complete.
+ @type current_param: str
+ @return: Possible completions
+ @rtype: list of str
+ '''
+ if current_param == 'topic':
+ # TODO Add other types of topics
+ topics = self.list_commands()
+ completions = [topic for topic in topics
+ if topic.startswith(text)]
+ else:
+ completions = []
+
+ if len(completions) == 1:
+ return [completions[0] + ' ']
+ else:
+ return completions
+
+ def ui_command_exit(self):
+ '''
+ Exits the command line interface.
+ '''
+ return 'EXIT'
+
+ def ui_command_bookmarks(self, action, bookmark=None):
+ '''
+ Manage your bookmarks.
+
+ Note that you can also access your bookmarks with the
+ B{cd} command. For instance, the following commands
+ are equivalent:
+ - B{cd mybookmark}
+ - C{bookmarks go mybookmark}
+
+ You can also use bookmarks anywhere where you would use
+ a normal path:
+ - B{@mybookmark ls} would perform the B{ls} command
+ in the bookmarked path.
+ - B{ls @mybookmark} would show you the objects tree from
+ the bookmarked path.
+
+
+ PARAMETERS
+ ==========
+
+ I{action}
+ ---------
+ The I{action} is one of:
+ - B{add} adds the current path to your bookmarks.
+ - B{del} deletes a bookmark.
+ - B{go} takes you to a bookmarked path.
+ - B{show} shows you all your bookmarks.
+
+ I{bookmark}
+ -----------
+ This is the name of the bookmark.
+
+ SEE ALSO
+ ========
+ ls cd
+ '''
+ if action == 'add' and bookmark:
+ if bookmark in self.shell.prefs['bookmarks']:
+ raise ExecutionError("Bookmark %s already exists." % bookmark)
+
+ self.shell.prefs['bookmarks'][bookmark] = self.path
+ # No way Prefs is going to account for that :-(
+ self.shell.prefs.save()
+ self.shell.log.info("Bookmarked %s as %s."
+ % (self.path, bookmark))
+ elif action == 'del' and bookmark:
+ if bookmark not in self.shell.prefs['bookmarks']:
+ raise ExecutionError("No such bookmark %s." % bookmark)
+
+ del self.shell.prefs['bookmarks'][bookmark]
+ # No way Prefs is going to account for that deletion
+ self.shell.prefs.save()
+ self.shell.log.info("Deleted bookmark %s." % bookmark)
+ elif action == 'go' and bookmark:
+ if bookmark not in self.shell.prefs['bookmarks']:
+ raise ExecutionError("No such bookmark %s." % bookmark)
+ return self.ui_command_cd(
+ self.shell.prefs['bookmarks'][bookmark])
+ elif action == 'show':
+ bookmarks = self.shell.con.dedent('''
+ BOOKMARKS
+ =========
+
+ ''')
+ if not self.shell.prefs['bookmarks']:
+ bookmarks += "No bookmarks yet.\n"
+ else:
+ for (bookmark, path) \
+ in six.iteritems(self.shell.prefs['bookmarks']):
+ if len(bookmark) == 1:
+ bookmark += '\0'
+ underline = ''.ljust(len(bookmark), '-')
+ bookmarks += "%s\n%s\n%s\n\n" % (bookmark, underline, path)
+ self.shell.con.epy_write(bookmarks)
+ else:
+ raise ExecutionError("Syntax error, see 'help bookmarks'.")
+
+ def ui_complete_bookmarks(self, parameters, text, current_param):
+ '''
+ Parameter auto-completion method for user command bookmarks.
+ @param parameters: Parameters on the command line.
+ @type parameters: dict
+ @param text: Current text of parameter being typed by the user.
+ @type text: str
+ @param current_param: Name of parameter to complete.
+ @type current_param: str
+ @return: Possible completions
+ @rtype: list of str
+ '''
+ if current_param == 'action':
+ completions = [action for action in ['add', 'del', 'go', 'show']
+ if action.startswith(text)]
+ elif current_param == 'bookmark':
+ if 'action' in parameters:
+ if parameters['action'] not in ['show', 'add']:
+ completions = [mark for mark
+ in self.shell.prefs['bookmarks']
+ if mark.startswith(text)]
+ else:
+ completions = []
+
+ if len(completions) == 1:
+ return [completions[0] + ' ']
+ else:
+ return completions
+
+ def ui_command_pwd(self):
+ '''
+ Displays the current path.
+
+ SEE ALSO
+ ========
+ ls cd
+ '''
+ self.shell.con.display(self.path)
+
+ # Private methods
+
+ def __str__(self):
+ if self.is_root():
+ return '/'
+ else:
+ return self.name
+
+ def _get_parent(self):
+ '''
+ Get this node's parent.
+ @return: The node's parent.
+ @rtype: ConfigNode
+ '''
+ return self._parent
+
+ def _get_name(self):
+ '''
+ @return: The node's name.
+ @rtype: str
+ '''
+ return self._name
+
+ def _set_name(self, name):
+ '''
+ Sets the node's name.
+ '''
+ self._name = name
+
+ def _get_path(self):
+ '''
+ @returns: The absolute path for this node.
+ @rtype: str
+ '''
+ subpath = self._path_separator + self.name
+ if self.is_root():
+ return self._path_separator
+ elif self._parent.is_root():
+ return subpath
+ else:
+ return self._parent.path + subpath
+
+ def _list_children(self):
+ '''
+ Lists the children of this node.
+ @return: The set of children nodes.
+ @rtype: set of ConfigNode
+ '''
+ return self._children
+
+ def _get_shell(self):
+ '''
+ Gets the shell attached to ConfigNode tree.
+ '''
+ if self.is_root():
+ return self._shell
+ else:
+ return self.get_root().shell
+
+ # Public methods
+
+ def summary(self):
+ '''
+ Returns a tuple with a status/description string for this node and a
+ health flag, to be displayed along the node's name in object trees,
+ etc.
+ @returns: (description, is_healthy)
+ @rtype: (str, bool or None)
+ '''
+ return ('', None)
+
+ def execute_command(self, command, pparams=[], kparams={}):
+ '''
+ Execute a user command on the node. This works by finding out which is
+ the support command method, using ConfigNode naming convention:
+ The support method's name is 'PREFIX_COMMAND', where PREFIX is defined
+ by ConfigNode.ui_command_method_prefix and COMMAND is the commands's
+ name as seen by the user.
+ @param command: Name of the command.
+ @type command: str
+ @param pparams: The positional parameters to use.
+ @type pparams: list
+ @param kparams: The keyword=value parameters to use.
+ @type kparams: dict
+ @return: The support method's return value.
+ See ConfigShell._execute_command() for expected return values and how
+ they are interpreted by ConfigShell.
+ @rtype: str or ConfigNode or None
+ '''
+ self.shell.log.debug("Executing command %s " % command
+ + "with pparams %s " % pparams
+ + "and kparams %s." % kparams)
+
+ if command in self.list_commands():
+ method = self.get_command_method(command)
+ else:
+ raise ExecutionError("Command not found %s" % command)
+
+ self.assert_params(method, pparams, kparams)
+ return method(*pparams, **kparams)
+
+ def assert_params(self, method, pparams, kparams):
+ '''
+ Checks that positional and keyword parameters match a method
+ definition, or raise an ExecutionError.
+ @param method: The method to check call signature against.
+ @type method: method
+ @param pparams: The positional parameters.
+ @type pparams: list
+ @param kparams: The keyword parameters.
+ @type kparams: dict
+ @raise ExecutionError: When the check fails.
+ '''
+ spec = inspect.getargspec(method)
+ args = spec.args[1:]
+ pp = spec.varargs
+ kw = spec.keywords
+
+ if spec.defaults is None:
+ nb_opt_params = 0
+ else:
+ nb_opt_params = len(spec.defaults)
+ nb_max_params = len(args)
+ nb_min_params = nb_max_params - nb_opt_params
+
+ req_params = args[:nb_min_params]
+ opt_params = args[nb_min_params:]
+
+ unexpected_keywords = sorted(set(kparams) - set(args))
+ missing_params = sorted(set(args[len(pparams):])
+ - set(opt_params)
+ - set(kparams.keys()))
+
+ nb_params = len(pparams) + len(kparams)
+ nb_standard_params = len(pparams) \
+ + len([param for param in kparams if param in args])
+ nb_extended_params = nb_params - nb_standard_params
+
+ self.shell.log.debug("Min params: %d" % nb_min_params)
+ self.shell.log.debug("Max params: %d" % nb_max_params)
+ self.shell.log.debug("Required params: %s" % ", ".join(req_params))
+ self.shell.log.debug("Optional params: %s" % ", ".join(opt_params))
+ self.shell.log.debug("Got %s standard params." % nb_standard_params)
+ self.shell.log.debug("Got %s extended params." % nb_extended_params)
+ self.shell.log.debug("Variable positional params: %s" % pp)
+ self.shell.log.debug("Variable keyword params: %s" % kw)
+
+ if len(missing_params) == 1:
+ raise ExecutionError(
+ "Missing required parameter %s"
+ % missing_params[0])
+ elif missing_params:
+ raise ExecutionError(
+ "Missing required parameters %s"
+ % ", ".join("'%s'" % missing for missing in missing_params))
+
+ if spec.keywords is None:
+ if len(unexpected_keywords) == 1:
+ raise ExecutionError(
+ "Unexpected keyword parameter '%s'."
+ % unexpected_keywords[0])
+ elif unexpected_keywords:
+ raise ExecutionError(
+ "Unexpected keyword parameters %s."
+ % ", ".join("'%s'" % kw for kw in unexpected_keywords))
+ all_params = args[:len(pparams)]
+ all_params.extend(kparams.keys())
+ for param in all_params:
+ if all_params.count(param) > 1:
+ raise ExecutionError(
+ "Duplicate parameter %s."
+ % param)
+
+ if nb_opt_params == 0 \
+ and nb_standard_params != nb_min_params \
+ and pp is None:
+ raise ExecutionError(
+ "Got %d positionnal parameters, expected exactly %d."
+ % (nb_standard_params, nb_min_params))
+
+ if nb_standard_params > nb_max_params and pp is None:
+ raise ExecutionError(
+ "Got %d positionnal parameters, expected at most %d."
+ % (nb_standard_params, nb_max_params))
+
+ def list_commands(self):
+ '''
+ @return: The list of user commands available for this node.
+ @rtype: list of str
+ '''
+ prefix = self.ui_command_method_prefix
+ prefix_len = len(prefix)
+ return tuple([name[prefix_len:] for name in dir(self)
+ if name.startswith(prefix) and name != prefix
+ and inspect.ismethod(getattr(self, name))])
+
+ def get_group_getter(self, group):
+ '''
+ @param group: A valid configuration group
+ @type group: str
+ @return: The getter method for the configuration group.
+ @rtype: method object
+ '''
+ prefix = self.ui_getgroup_method_prefix
+ return getattr(self, '%s%s' % (prefix, group))
+
+ def get_group_setter(self, group):
+ '''
+ @param group: A valid configuration group
+ @type group: str
+ @return: The setter method for the configuration group.
+ @rtype: method object
+ '''
+ prefix = self.ui_setgroup_method_prefix
+ return getattr(self, '%s%s' % (prefix, group))
+
+ def get_command_method(self, command):
+ '''
+ @param command: The command to get the method for.
+ @type command: str
+ @return: The user command support method.
+ @rtype: method
+ @raise ValueError: If the command is not found.
+ '''
+ prefix = self.ui_command_method_prefix
+ if command in self.list_commands():
+ return getattr(self, '%s%s' % (prefix, command))
+ else:
+ self.shell.log.debug('No command named %s in %s (%s)'
+ % (command, self.name, self.path))
+ raise ValueError('No command named "%s".' % command)
+
+ def get_completion_method(self, command):
+ '''
+ @return: A user command's completion method or None.
+ @rtype: method or None
+ @param command: The command to get the completion method for.
+ @type command: str
+ '''
+ prefix = self.ui_complete_method_prefix
+ try:
+ method = getattr(self, '%s%s' % (prefix, command))
+ except AttributeError:
+ return None
+ else:
+ return method
+
+ def get_command_description(self, command):
+ '''
+ @return: An description string for a user command.
+ @rtype: str
+ @param command: The command to describe.
+ @type command: str
+ '''
+ doc = self.get_command_method(command).__doc__
+ if not doc:
+ doc = "No description available."
+ return self.shell.con.dedent(doc)
+
+ def get_command_syntax(self, command):
+ '''
+ @return: A list of formatted syntax descriptions for the command:
+ - (syntax, comments, default_values)
+ - syntax is the syntax definition line.
+ - comments is a list of additionnal comments about the syntax.
+ - default_values is a string with the default parameters values.
+ @rtype: (str, [str...], str)
+ @param command: The command to document.
+ @type command: str
+ '''
+ method = self.get_command_method(command)
+ parameters, args, kwargs, default = inspect.getargspec(method)
+ parameters = parameters[1:]
+ if default is None:
+ num_defaults = 0
+ else:
+ num_defaults = len(default)
+
+ if num_defaults != 0:
+ required_parameters = parameters[:-num_defaults]
+ optional_parameters = parameters[-num_defaults:]
+ else:
+ required_parameters = parameters
+ optional_parameters = []
+
+ self.shell.log.debug("Required: %s" % str(required_parameters))
+ self.shell.log.debug("Optional: %s" % str(optional_parameters))
+
+ syntax = "B{%s} " % command
+
+ required_parameters_str = ''
+ for param in required_parameters:
+ required_parameters_str += "I{%s} " % param
+ syntax += required_parameters_str
+
+ optional_parameters_str = ''
+ for param in optional_parameters:
+ optional_parameters_str += "[I{%s}] " % param
+ syntax += optional_parameters_str
+
+ comments = []
+ #if optional_parameters:
+ # comments.append(self.shell.con.dedent(
+ # '''
+ # %s - These are optional parameters that can either be
+ # specified in the above order as positional parameters, or in
+ # any order at the end of the line as keyword=value parameters.
+ # ''' % optional_parameters_str[:-1]))
+
+ if args is not None:
+ syntax += "[I{%s}...] " % args
+ # comments.append(self.shell.con.dedent(
+ # '''
+ # [I{%s}...] - This command accepts an arbitrary number of
+ # parameters before any keyword=value parameter. In order to use
+ # them, you must fill in all previous positional parameters if
+ # any. See B{DESCRIPTION} below.
+ # ''' % args))
+
+ if kwargs is not None:
+ syntax += "[I{%s=value}...] " % (kwargs)
+ # comments.append(self.shell.con.dedent(
+ # '''
+ # This command also accepts an arbitrary number of
+ # keyword=value parameters. See B{DESCRIPTION} below.
+ # '''))
+
+ default_values = ''
+ if num_defaults > 0:
+ for index, param in enumerate(optional_parameters):
+ if default[index] is not None:
+ default_values += "%s=%s " % (param, str(default[index]))
+
+ return syntax, comments, default_values
+
+ def get_command_signature(self, command):
+ '''
+ Get a command's signature.
+ @param command: The command to get the signature of.
+ @type command: str
+ @return: (parameters, free_pparams, free_kparams) where parameters is a
+ list of all the command's parameters and free_pparams and free_kparams
+ booleans set to True is the command accepts an arbitrary number of,
+ respectively, pparams and kparams.
+ @rtype: ([str...], bool, bool)
+ '''
+ method = self.get_command_method(command)
+ parameters, args, kwargs, default = inspect.getargspec(method)
+ parameters = parameters[1:]
+ if args is not None:
+ free_pparams = args
+ else:
+ free_pparams = False
+ if kwargs is not None:
+ free_kparams = kwargs
+ else:
+ free_kparams = False
+ self.shell.log.debug("Signature is %s, %s, %s."
+ % (str(parameters),
+ str(free_pparams),
+ str(free_kparams)))
+ return parameters, free_pparams, free_kparams
+
+ def get_root(self):
+ '''
+ @return: The root node of the nodes tree.
+ @rtype: ConfigNode
+ '''
+ if self.is_root():
+ return self
+ else:
+ return self.parent.get_root()
+
+ def define_config_group_param(self, group, param, type,
+ description=None, writable=True):
+ '''
+ Helper to define configuration group parameters.
+ @param group: The configuration group to add the parameter to.
+ @type group: str
+ @param param: The new parameter name.
+ @type param: str
+ @param description: Optional description string.
+ @type description: str
+ @param writable: Whether or not this would be a rw or ro parameter.
+ @type writable: bool
+ '''
+ if group not in self._configuration_groups:
+ self._configuration_groups[group] = {}
+
+ if description is None:
+ description = "The %s %s parameter." % (param, group)
+
+ # Fail early if the type and set/get helpers don't exist
+ self.get_type_method(type)
+ self.get_group_getter(group)
+ if writable:
+ self.get_group_setter(group)
+
+ self._configuration_groups[group][param] = \
+ [type, description, writable]
+
+ def list_config_groups(self):
+ '''
+ Lists the configuration group names.
+ '''
+ return self._configuration_groups.keys()
+
+ def list_group_params(self, group, writable=None):
+ '''
+ Lists the parameters from group matching the optional param, writable
+ and type supplied (if none is supplied, returns all group parameters.
+ @param group: The group to list parameters of.
+ @type group: str
+ @param writable: Optional writable flag filter.
+ @type writable: bool
+ '''
+ if group not in self.list_config_groups():
+ return []
+ else:
+ params = []
+ for p_name, p_def in six.iteritems(self._configuration_groups[group]):
+ (p_type, p_description, p_writable) = p_def
+ if writable is not None and p_writable != writable:
+ continue
+ params.append(p_name)
+
+ params.sort()
+ return params
+
+ def get_group_param(self, group, param):
+ '''
+ @param group: The configuration group to retreive the parameter from.
+ @type group: str
+ @param param: The parameter name.
+ @type param: str
+ @return: A dictionnary for the requested group parameter, with
+ name, writable, description, group and type fields.
+ @rtype: dict
+ @raise ValueError: If the parameter or group does not exist.
+ '''
+ if group not in self.list_config_groups():
+ raise ValueError("Not such configuration group %s" % group)
+ if param not in self.list_group_params(group):
+ raise ValueError("Not such parameter %s in configuration group %s"
+ % (param, group))
+ (p_type, p_description, p_writable) = \
+ self._configuration_groups[group][param]
+
+ return dict(name=param, group=group, type=p_type,
+ description=p_description, writable=p_writable)
+
+ shell = property(_get_shell,
+ doc="Gets the shell attached to ConfigNode tree.")
+
+ name = property(_get_name, _set_name,
+ doc="Gets or sets the node's name.")
+
+ path = property(_get_path,
+ doc="Gets the node's path.")
+
+ children = property(_list_children,
+ doc="Lists the node's children.")
+
+ parent = property(_get_parent,
+ doc="Gets the node's parent.")
+
+ def is_root(self):
+ '''
+ @return: Wether or not we are a root node.
+ @rtype: bool
+ '''
+ if self._parent is None:
+ return True
+ else:
+ return False
+
+ def get_child(self, name):
+ '''
+ @param name: The child's name.
+ @type name: str
+ @return: Our child named by name.
+ @rtype: ConfigNode
+ @raise ValueError: If there is no child named by name.
+ '''
+ for child in self._children:
+ if child.name == name:
+ return child
+ else:
+ raise ValueError("No such path %s/%s"
+ % (self.path.rstrip('/'), name))
+
+ def remove_child(self, child):
+ '''
+ Removes a child from our children's list.
+ @param child: The child to remove.
+ @type child: ConfigNode
+ '''
+ self._children.remove(child)
+
+ def get_node(self, path):
+ '''
+ Looks up a node by path in the nodes tree.
+ @param path: The node's path.
+ @type path: str
+ @return: The node that has the given path.
+ @rtype: ConfigNode
+ @raise ValueError: If there is no node with that path.
+ '''
+ def adjacent_node(name):
+ '''
+ Returns an adjacent node or ourself.
+ '''
+ if name == self._path_current:
+ return self
+ elif name == self._path_previous:
+ if self._parent is not None:
+ return self._parent
+ else:
+ return self
+ else:
+ return self.get_child(name)
+
+
+ # Cleanup the path
+ if path is None or path == '':
+ path = '.'
+
+ # Is it a bookmark ?
+ if path.startswith('@'):
+ bookmark = path.lstrip('@').strip()
+ if bookmark in self.shell.prefs['bookmarks']:
+ path = self.shell.prefs['bookmarks'][bookmark]
+ else:
+ raise ValueError("No such bookmark %s" % bookmark)
+
+ # More cleanup
+ path = re.sub('%s+' % self._path_separator, self._path_separator, path)
+ if len(path) > 1:
+ path = path.rstrip(self._path_separator)
+ self.shell.log.debug("Looking for path '%s'" % path)
+
+
+ # Absolute path - make relative and pass on to root node
+ if path.startswith(self._path_separator):
+ next_node = self.get_root()
+ next_path = path.lstrip(self._path_separator)
+ if next_path:
+ return next_node.get_node(next_path)
+ else:
+ return next_node
+
+ # Relative path
+ if self._path_separator in path:
+ next_node_name, next_path = path.split(self._path_separator, 1)
+ next_node = adjacent_node(next_node_name)
+ return next_node.get_node(next_path)
+
+ # Path is just one of our children
+ return adjacent_node(path)
diff --git a/configshell/prefs.py b/configshell/prefs.py
new file mode 100644
index 0000000..248cc1d
--- /dev/null
+++ b/configshell/prefs.py
@@ -0,0 +1,149 @@
+'''
+This file is part of ConfigShell.
+Copyright (c) 2011-2013 by Datera, Inc
+
+Licensed under the Apache License, Version 2.0 (the "License"); you may
+not use this file except in compliance with the License. You may obtain
+a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+License for the specific language governing permissions and limitations
+under the License.
+'''
+
+import six
+
+class Prefs(object):
+ '''
+ This is a preferences backend object used to:
+ - Hold the ConfigShell preferences
+ - Handle persistent storage and retrieval of these preferences
+ - Share the preferences between the ConfigShell and ConfigNode objects
+
+ As it is inherently destined to be shared between objects, this is a Borg.
+ '''
+ _prefs = {}
+ filename = None
+ autosave = False
+ __borg_state = {}
+
+ def __init__(self, filename=None):
+ '''
+ Instanciates the ConfigShell preferences object.
+ @param filename: File to store the preferencces to.
+ @type filename: str
+ '''
+ self.__dict__ = self.__borg_state
+ if filename is not None:
+ self.filename = filename
+
+ def __getitem__(self, key):
+ '''
+ Proxies dict-like references to prefs.
+ One specific behavior, though, is that if the key does not exists,
+ we will return None instead of raising an exception.
+ @param key: The preferences dictionnary key to get.
+ @type key: any valid dict key
+ @return: The key value
+ @rtype: n/a
+ '''
+ if key in self._prefs:
+ return self._prefs[key]
+ else:
+ return None
+
+ def __setitem__(self, key, value):
+ '''
+ Proxies dict-like references to prefs.
+ @param key: The preferences dictionnary key to set.
+ @type key: any valid dict key
+ '''
+ self._prefs[key] = value
+ if self.autosave:
+ self.save()
+
+ def __contains__(self, key):
+ '''
+ Do the preferences contain key ?
+ @param key: The preferences dictionnary key to check.
+ @type key: any valid dict key
+ '''
+ if key in self._prefs:
+ return True
+ else:
+ return False
+
+ def __delitem__(self, key):
+ '''
+ Deletes a preference key.
+ @param key: The preference to delete.
+ @type key: any valid dict key
+ '''
+ del self._prefs[key]
+ if self.autosave:
+ self.save()
+
+ def __iter__(self):
+ '''
+ Generic iterator for the preferences.
+ '''
+ return self._prefs.__iter__()
+
+ # Public methods
+
+ def keys(self):
+ '''
+ @return: Returns the list of keys in preferences.
+ @rtype: list
+ '''
+ return self._prefs.keys()
+
+ def items(self):
+ '''
+ @return: Returns the list of items in preferences.
+ @rtype: list of (key, value) tuples
+ '''
+ return self._prefs.items()
+
+ def iteritems(self):
+ '''
+ @return: Iterates on the items in preferences.
+ @rtype: yields items that are (key, value) pairs
+ '''
+ return six.iteritems(self._prefs)
+
+ def save(self, filename=None):
+ '''
+ Saves the preferences to disk. If filename is not specified,
+ use the default one if it is set, else do nothing.
+ @param filename: Optional alternate file to use.
+ @type filename: str
+ '''
+ if filename is None:
+ filename = self.filename
+
+ if filename is not None:
+ fsock = open(filename, 'wb')
+ try:
+ six.moves.cPickle.dump(self._prefs, fsock, 2)
+ finally:
+ fsock.close()
+
+ def load(self, filename=None):
+ '''
+ Loads the preferences from file. Use either the supplied filename,
+ or the default one if set. Else, do nothing.
+ '''
+ if filename is None:
+ filename = self.filename
+
+ if filename is not None:
+ fsock = open(filename, 'rb')
+ try:
+ self._prefs = six.moves.cPickle.load(fsock)
+ finally:
+ fsock.close()
diff --git a/configshell/shell.py b/configshell/shell.py
new file mode 100644
index 0000000..32701ec
--- /dev/null
+++ b/configshell/shell.py
@@ -0,0 +1,907 @@
+'''
+This file is part of ConfigShell.
+Copyright (c) 2011-2013 by Datera, Inc
+
+Licensed under the Apache License, Version 2.0 (the "License"); you may
+not use this file except in compliance with the License. You may obtain
+a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+License for the specific language governing permissions and limitations
+under the License.
+'''
+
+import os
+import six
+import sys
+from pyparsing import (alphanums, Empty, Group, OneOrMore, Optional,
+ ParseResults, Regex, Suppress, Word)
+
+from . import console
+from . import log
+from . import prefs
+from .node import ConfigNode, ExecutionError
+
+# A fix for frozen packages
+import signal
+def handle_sigint(signum, frame):
+ '''
+ Raise KeyboardInterrupt when we get a SIGINT.
+ This is normally done by python, but even after patching
+ pyinstaller 1.4 to ignore SIGINT in the C wrapper code, we
+ still have to do the translation ourselves.
+ '''
+ raise KeyboardInterrupt
+
+try:
+ signal.signal(signal.SIGINT, handle_sigint)
+except Exception:
+ # In a thread, this fails
+ pass
+
+if sys.stdout.isatty():
+ import readline
+ tty=True
+else:
+ tty=False
+
+# Pyparsing helper to group the location of a token and its value
+# http://stackoverflow.com/questions/18706631/pyparsing-get-token-location-in-results-name
+locator = Empty().setParseAction(lambda s, l, t: l)
+def locatedExpr(expr):
+ return Group(locator('location') + expr('value'))
+
+class ConfigShell(object):
+ '''
+ This is a simple CLI command interpreter that can be used both in
+ interactive or non-interactive modes.
+ It is based on a tree of ConfigNode objects, which can be navigated.
+
+ The ConfigShell object itself provides global navigation commands.
+ It also handles the parsing of local commands (specific to a certain
+ ConfigNode) according to the ConfigNode commands definitions.
+ If the ConfigNode provides hooks for possible parameter values in a given
+ context, then the ConfigShell will also provide command-line completion
+ using the TAB key. If no completion hooks are available from the
+ ConfigNode, the completion function will still be able to display some help
+ and general syntax advice (as much as the ConfigNode will provide).
+
+ Interactive sessions can be saved/loaded automatically by ConfigShell is a
+ writable session directory is supplied. This includes command-line history,
+ current node and global parameters.
+ '''
+
+ default_prefs = {'color_path': 'magenta',
+ 'color_command': 'cyan',
+ 'color_parameter': 'magenta',
+ 'color_keyword': 'cyan',
+ 'logfile': None,
+ 'loglevel_console': 'info',
+ 'loglevel_file': 'debug9',
+ 'color_mode': True,
+ 'prompt_length': 30,
+ 'tree_max_depth': 0,
+ 'tree_status_mode': True,
+ 'tree_round_nodes': True,
+ 'tree_show_root': True
+ }
+
+ _completion_help_topic = ''
+ _current_parameter = ''
+ _current_token = ''
+ _current_completions = []
+
+ def __init__(self, preferences_dir=None):
+ '''
+ Creates a new ConfigShell.
+ @param preferences_dir: Directory to load/save preferences from/to
+ @type preferences_dir: str
+ '''
+ self._current_node = None
+ self._root_node = None
+ self._exit = False
+
+ # Grammar of the command line
+ command = locatedExpr(Word(alphanums + '_'))('command')
+ var = Word(alphanums + '_\+/.<>()~@:-%[]')
+ value = var
+ keyword = Word(alphanums + '_\-')
+ kparam = locatedExpr(keyword + Suppress('=') + Optional(value, default=''))('kparams*')
+ pparam = locatedExpr(var)('pparams*')
+ parameter = kparam | pparam
+ parameters = OneOrMore(parameter)
+ bookmark = Regex('@([A-Za-z0-9:_.]|-)+')
+ pathstd = Regex('([A-Za-z0-9:_.]|-)*' + '/' + '([A-Za-z0-9:_./]|-)*') \
+ | '..' | '.'
+ path = locatedExpr(bookmark | pathstd | '*')('path')
+ parser = Optional(path) + Optional(command) + Optional(parameters)
+ self._parser = parser
+
+ if tty:
+ readline.set_completer_delims('\t\n ~!#$^&(){}\|;\'",?')
+ readline.set_completion_display_matches_hook(
+ self._display_completions)
+
+ self.log = log.Log()
+
+ if preferences_dir is not None:
+ preferences_dir = os.path.expanduser(preferences_dir)
+ if not os.path.exists(preferences_dir):
+ os.makedirs(preferences_dir)
+ self._prefs_file = preferences_dir + '/prefs.bin'
+ self.prefs = prefs.Prefs(self._prefs_file)
+ self._cmd_history = preferences_dir + '/history.txt'
+ self._save_history = True
+ if not os.path.isfile(self._cmd_history):
+ try:
+ open(self._cmd_history, 'w').close()
+ except:
+ self.log.warning("Cannot create history file %s, "
+ % self._cmd_history
+ + "command history will not be saved.")
+ self._save_history = False
+
+ if os.path.isfile(self._cmd_history) and tty:
+ try:
+ readline.read_history_file(self._cmd_history)
+ except IOError:
+ self.log.warning("Cannot read command history file %s."
+ % self._cmd_history)
+
+ if self.prefs['logfile'] is None:
+ self.prefs['logfile'] = preferences_dir + '/' + 'log.txt'
+
+ self.prefs.autosave = True
+
+ else:
+ self.prefs = prefs.Prefs()
+ self._save_history = False
+
+ try:
+ self.prefs.load()
+ except IOError:
+ self.log.warning("Could not load preferences file %s."
+ % self._prefs_file)
+
+ for pref, value in six.iteritems(self.default_prefs):
+ if pref not in self.prefs:
+ self.prefs[pref] = value
+
+ self.con = console.Console()
+
+ # Private methods
+
+ def _display_completions(self, substitution, matches, max_length):
+ '''
+ Display the completions. Invoked by readline.
+ @param substitution: string to complete
+ @param matches: list of possible matches
+ @param max_length: length of the longest matching item
+ '''
+ x_orig = self.con.get_cursor_xy()[0]
+ width = self.con.get_width()
+ max_length += 2
+
+ def just(text):
+ '''
+ Justifies the text to the max match length.
+ '''
+ return text.ljust(max_length, " ")
+
+ # Sort and colorize the matches
+ if self._current_parameter:
+ keywords = []
+ values = []
+ for match in matches:
+ if match.endswith('='):
+ keywords.append(
+ self.con.render_text(
+ just(match), self.prefs['color_keyword']))
+ elif '=' in match:
+ _, _, value = match.partition('=')
+ values.append(
+ self.con.render_text(
+ just(value), self.prefs['color_parameter']))
+ else:
+ values.append(
+ self.con.render_text(
+ just(match), self.prefs['color_parameter']))
+ matches = values + keywords
+ else:
+ paths = []
+ commands = []
+ for match in matches:
+ if '/' in match or match.startswith('@') or '*' in match:
+ paths.append(
+ self.con.render_text(
+ just(match), self.prefs['color_path']))
+ else:
+ commands.append(
+ self.con.render_text(
+ just(match), self.prefs['color_command']))
+ matches = paths + commands
+
+ # Display the possible completions in columns
+ self.con.raw_write("\n")
+ if matches:
+ if max_length < width:
+ nr_cols = width // max_length
+ else:
+ nr_cols = 1
+
+ for i in six.moves.range(0, len(matches), nr_cols):
+ self.con.raw_write(''.join(matches[i:i+nr_cols]))
+ self.con.raw_write('\n')
+
+ # Display the prompt and the command line
+ line = "%s%s" % (self._get_prompt(), readline.get_line_buffer())
+ self.con.raw_write("%s" % line)
+
+ # Move the cursor where it should be
+ y_pos = self.con.get_cursor_xy()[1]
+ self.con.set_cursor_xy(x_orig, y_pos)
+
+ def _complete_token_command(self, text, path, command):
+ '''
+ Completes a partial command token, which could also be the beginning
+ of a path.
+ @param path: Path of the target ConfigNode.
+ @type path: str
+ @param command: The command (if any) found by the parser.
+ @type command: str
+ @param text: Current text being typed by the user.
+ @type text: str
+ @return: Possible completions for the token.
+ @rtype: list of str
+ '''
+ completions = []
+ target = self._current_node.get_node(path)
+ commands = target.list_commands()
+ self.log.debug("Completing command token among %s" % str(commands))
+
+ # Start with the possible commands
+ for command in commands:
+ if command.startswith(text):
+ completions.append(command)
+ if len(completions) == 1:
+ completions[0] = completions[0] + ' '
+
+ # No identified path yet on the command line, this might be it
+ if not path:
+ path_completions = [child.name + '/'
+ for child in self._current_node.children
+ if child.name.startswith(text)]
+ if not text:
+ path_completions.append('/')
+ if len(self._current_node.children) > 1:
+ path_completions.append('* ')
+
+ if path_completions:
+ if completions:
+ self._current_token = \
+ self.con.render_text(
+ 'path', self.prefs['color_path']) \
+ + '|' \
+ + self.con.render_text(
+ 'command', self.prefs['color_command'])
+ else:
+ self._current_token = \
+ self.con.render_text(
+ 'path', self.prefs['color_path'])
+ else:
+ self._current_token = \
+ self.con.render_text(
+ 'command', self.prefs['color_command'])
+ if len(path_completions) == 1 and \
+ not path_completions[0][-1] in [' ', '*'] and \
+ not self._current_node.get_node(path_completions[0]).children:
+ path_completions[0] = path_completions[0] + ' '
+ completions.extend(path_completions)
+ else:
+ self._current_token = \
+ self.con.render_text(
+ 'command', self.prefs['color_command'])
+
+ # Even a bookmark
+ bookmarks = ['@' + bookmark for bookmark in self.prefs['bookmarks']
+ if bookmark.startswith("%s" % text.lstrip('@'))]
+ self.log.debug("Found bookmarks %s." % str(bookmarks))
+ if bookmarks:
+ completions.extend(bookmarks)
+
+
+ # We are done
+ return completions
+
+ def _complete_token_path(self, text):
+ '''
+ Completes a partial path token.
+ @param text: Current text being typed by the user.
+ @type text: str
+ @return: Possible completions for the token.
+ @rtype: list of str
+ '''
+ completions = []
+ if text.endswith('.'):
+ text = text + '/'
+ (basedir, slash, partial_name) = text.rpartition('/')
+ self.log.debug("Got basedir=%s, partial_name=%s"
+ % (basedir, partial_name))
+ basedir = basedir + slash
+ target = self._current_node.get_node(basedir)
+ names = [child.name for child in target.children]
+
+ # Iterall path completion
+ if names and partial_name in ['', '*']:
+ # Not suggesting iterall to end a path that has only one
+ # child allows for fast TAB action to add the only child's
+ # name.
+ if len(names) > 1:
+ completions.append("%s* " % basedir)
+
+ for name in names:
+ num_matches = 0
+ if name.startswith(partial_name):
+ num_matches += 1
+ if num_matches == 1:
+ completions.append("%s%s/" % (basedir, name))
+ else:
+ completions.append("%s%s" % (basedir, name))
+
+ # Bookmarks
+ bookmarks = ['@' + bookmark for bookmark in self.prefs['bookmarks']
+ if bookmark.startswith("%s" % text.lstrip('@'))]
+ self.log.debug("Found bookmarks %s." % str(bookmarks))
+ if bookmarks:
+ completions.extend(bookmarks)
+
+ if len(completions) == 1:
+ self.log.debug("One completion left.")
+ if not completions[0].endswith("* "):
+ if not self._current_node.get_node(completions[0]).children:
+ completions[0] = completions[0].rstrip('/') + ' '
+
+ self._current_token = \
+ self.con.render_text(
+ 'path', self.prefs['color_path'])
+ return completions
+
+ def _complete_token_pparam(self, text, path, command, pparams, kparams):
+ '''
+ Completes a positional parameter token, which can also be the keywork
+ part of a kparam token, as before the '=' sign is on the line, the
+ parser cannot know better.
+ @param path: Path of the target ConfigNode.
+ @type path: str
+ @param command: The command (if any) found by the parser.
+ @type command: str
+ @param pparams: Positional parameters from commandline.
+ @type pparams: list of str
+ @param kparams: Keyword parameters from commandline.
+ @type kparams: dict of str:str
+ @param text: Current text being typed by the user.
+ @type text: str
+ @return: Possible completions for the token.
+ @rtype: list of str
+ '''
+ completions = []
+ target = self._current_node.get_node(path)
+ cmd_params, free_pparams, free_kparams = \
+ target.get_command_signature(command)
+ current_parameters = {}
+ for index in range(len(pparams)):
+ if index < len(cmd_params):
+ current_parameters[cmd_params[index]] = pparams[index]
+ for key, value in six.iteritems(kparams):
+ current_parameters[key] = value
+ self._completion_help_topic = command
+ completion_method = target.get_completion_method(command)
+ self.log.debug("Command %s accepts parameters %s."
+ % (command, cmd_params))
+
+ # Do we still accept positional params ?
+ pparam_ok = True
+ for index in range(len(cmd_params)):
+ param = cmd_params[index]
+ if param in kparams:
+ if index <= len(pparams):
+ pparam_ok = False
+ self.log.debug(
+ "No more possible pparams (because of kparams).")
+ break
+ elif (text.strip() == '' and len(pparams) == len(cmd_params)) \
+ or (len(pparams) > len(cmd_params)):
+ pparam_ok = False
+ self.log.debug("No more possible pparams.")
+ break
+ else:
+ if len(cmd_params) == 0:
+ pparam_ok = False
+ self.log.debug("No more possible pparams (none exists)")
+
+ # If we do, find out which one we are completing
+ if pparam_ok:
+ if not text:
+ pparam_index = len(pparams)
+ else:
+ pparam_index = len(pparams) - 1
+ self._current_parameter = cmd_params[pparam_index]
+ self.log.debug("Completing pparam %s." % self._current_parameter)
+ if completion_method:
+ pparam_completions = completion_method(
+ current_parameters, text, self._current_parameter)
+ if pparam_completions is not None:
+ completions.extend(pparam_completions)
+
+ # Add the keywords for parameters not already on the line
+ if text:
+ offset = 1
+ else:
+ offset = 0
+ keyword_completions = [param + '=' \
+ for param in cmd_params[len(pparams)-offset:] \
+ if param not in kparams \
+ if param.startswith(text)]
+
+ self.log.debug("Possible pparam values are %s."
+ % str(completions))
+ self.log.debug("Possible kparam keywords are %s."
+ % str(keyword_completions))
+
+ if keyword_completions:
+ if self._current_parameter:
+ self._current_token = \
+ self.con.render_text(
+ self._current_parameter, \
+ self.prefs['color_parameter']) \
+ + '|' \
+ + self.con.render_text(
+ 'keyword=', self.prefs['color_keyword'])
+ else:
+ self._current_token = \
+ self.con.render_text(
+ 'keyword=', self.prefs['color_keyword'])
+ else:
+ if self._current_parameter:
+ self._current_token = \
+ self.con.render_text(
+ self._current_parameter,
+ self.prefs['color_parameter'])
+ else:
+ self._current_token = ''
+
+ completions.extend(keyword_completions)
+
+ if free_kparams or free_pparams:
+ self.log.debug("Command has free [kp]params.")
+ if completion_method:
+ self.log.debug("Calling completion method for free params.")
+ free_completions = completion_method(
+ current_parameters, text, '*')
+ do_free_pparams = False
+ do_free_kparams = False
+ for free_completion in free_completions:
+ if free_completion.endswith("="):
+ do_free_kparams = True
+ else:
+ do_free_pparams = True
+
+ if do_free_pparams:
+ self._current_token = \
+ self.con.render_text(
+ free_pparams, self.prefs['color_parameter']) \
+ + '|' + self._current_token
+ self._current_token = self._current_token.rstrip('|')
+ if not self._current_parameter:
+ self._current_parameter = 'free_parameter'
+
+ if do_free_kparams:
+ if not 'keyword=' in self._current_token:
+ self._current_token = \
+ self.con.render_text(
+ 'keyword=', self.prefs['color_keyword']) \
+ + '|' + self._current_token
+ self._current_token = self._current_token.rstrip('|')
+ if not self._current_parameter:
+ self._current_parameter = 'free_parameter'
+
+ completions.extend(free_completions)
+
+ self.log.debug("Found completions %s." % str(completions))
+ return completions
+
+ def _complete_token_kparam(self, text, path, command, pparams, kparams):
+ '''
+ Completes a keyword=value parameter token.
+ @param path: Path of the target ConfigNode.
+ @type path: str
+ @param command: The command (if any) found by the parser.
+ @type command: str
+ @param pparams: Positional parameters from commandline.
+ @type pparams: list of str
+ @param kparams: Keyword parameters from commandline.
+ @type kparams: dict of str:str
+ @param text: Current text being typed by the user.
+ @type text: str
+ @return: Possible completions for the token.
+ @rtype: list of str
+ '''
+ self.log.debug("Called for text='%s'" % text)
+ target = self._current_node.get_node(path)
+ cmd_params = target.get_command_signature(command)[0]
+ self.log.debug("Command %s accepts parameters %s."
+ % (command, cmd_params))
+
+ (keyword, sep, current_value) = text.partition('=')
+ self.log.debug("Completing '%s' for kparam %s"
+ % (current_value, keyword))
+
+ self._current_parameter = keyword
+ current_parameters = {}
+ for index in range(len(pparams)):
+ current_parameters[cmd_params[index]] = pparams[index]
+ for key, value in six.iteritems(kparams):
+ current_parameters[key] = value
+ completion_method = target.get_completion_method(command)
+ if completion_method:
+ completions = completion_method(
+ current_parameters, current_value, keyword)
+ if completions is None:
+ completions = []
+
+ self._current_token = \
+ self.con.render_text(
+ self._current_parameter, self.prefs['color_parameter'])
+
+ self.log.debug("Found completions %s." % str(completions))
+
+ return ["%s=%s" % (keyword, completion) for completion in completions]
+
+ def _complete(self, text, state):
+ '''
+ Text completion method, directly called by readline.
+ Finds out what token the user wants completion for, and calls the
+ _dispatch_completion() to get the possible completions.
+ Then implements the state system needed by readline to return those
+ possible completions to readline.
+ @param text: The text to complete.
+ @type text: str
+ @returns: The next possible completion for text.
+ @rtype: str
+ '''
+ if state == 0:
+ cmdline = readline.get_line_buffer()
+ self._current_completions = []
+ self._completion_help_topic = ''
+ self._current_parameter = ''
+
+ (parse_results, path, command, pparams, kparams) = \
+ self._parse_cmdline(cmdline)
+
+ beg = readline.get_begidx()
+ end = readline.get_endidx()
+ current_token = None
+ if beg == end:
+ # No text under the cursor, fake it so that the parser
+ # result_trees gives us a token name on a second parser call
+ self.log.debug("Faking text entry on commandline.")
+ parse_results = self._parse_cmdline(cmdline + 'x')[0]
+
+ if parse_results.command.value == 'x':
+ current_token = 'command'
+ elif 'x' in [x.value for x in parse_results.pparams]:
+ current_token = 'pparam'
+ elif 'x' in [x.value for x in parse_results.kparams]:
+ current_token = 'kparam'
+ elif path and beg == parse_results.path.location:
+ current_token = 'path'
+ elif command and beg == parse_results.command.location:
+ current_token = 'command'
+ elif pparams and beg in [p.location for p in parse_results.pparams]:
+ current_token = 'pparam'
+ elif kparams and beg in [k.location for k in parse_results.kparams]:
+ current_token = 'kparam'
+
+ self._current_completions = \
+ self._dispatch_completion(path, command,
+ pparams, kparams,
+ text, current_token)
+
+ self.log.debug("Returning completions %s to readline."
+ % str(self._current_completions))
+
+ if state < len(self._current_completions):
+ return self._current_completions[state]
+ else:
+ return None
+
+ def _dispatch_completion(self, path, command,
+ pparams, kparams, text, current_token):
+ '''
+ This method takes care of dispatching the current completion request
+ from readline (via the _complete() method) to the relevant token
+ completion methods. It has to cope with the fact that the commandline
+ being incomplete yet,
+ Of course, as the command line is still unfinished, the parser can
+ only do so much of a job. For instance, until the '=' sign is on the
+ command line, there is no way to distinguish a positional parameter
+ from the begining of a keyword=value parameter.
+ @param path: Path of the target ConfigNode.
+ @type path: str
+ @param command: The command (if any) found by the parser.
+ @type command: str
+ @param pparams: Positional parameters from commandline.
+ @type pparams: list of str
+ @param kparams: Keyword parameters from commandline.
+ @type kparams: dict of str:str
+ @param text: Current text being typed by the user.
+ @type text: str
+ @param current_token: Name of token to complete.
+ @type current_token: str
+ @return: Possible completions for the token.
+ @rtype: list of str
+ '''
+ completions = []
+
+ self.log.debug("Dispatching completion for %s token. "
+ % current_token
+ + "text='%s', path='%s', command='%s', "
+ % (text, path, command)
+ + "pparams=%s, kparams=%s"
+ % (str(pparams), str(kparams)))
+
+ (path, iterall) = path.partition('*')[:2]
+ if iterall:
+ try:
+ target = self._current_node.get_node(path)
+ except ValueError:
+ cpl_path = path
+ else:
+ children = target.children
+ if children:
+ cpl_path = children[0].path
+ else:
+ cpl_path = path
+
+
+ if current_token == 'command':
+ completions = self._complete_token_command(text, cpl_path, command)
+ elif current_token == 'path':
+ completions = self._complete_token_path(text)
+ elif current_token == 'pparam':
+ completions = \
+ self._complete_token_pparam(text, cpl_path, command,
+ pparams, kparams)
+ elif current_token == 'kparam':
+ completions = \
+ self._complete_token_kparam(text, cpl_path, command,
+ pparams, kparams)
+ else:
+ self.log.debug("Cannot complete unknown token %s."
+ % current_token)
+
+ return completions
+
+ def _get_prompt(self):
+ '''
+ Returns the command prompt string.
+ '''
+ prompt_path = self._current_node.path
+ prompt_length = self.prefs['prompt_length']
+
+ if prompt_length and prompt_length < len(prompt_path):
+ half = (prompt_length - 3) // 2
+ prompt_path = "%s...%s" \
+ % (prompt_path[:half], prompt_path[-half:])
+
+ if 'prompt_msg' in dir(self._current_node):
+ return "%s%s> " % (self._current_node.prompt_msg(),
+ prompt_path)
+ else:
+ return "%s> " % prompt_path
+
+ def _cli_loop(self):
+ '''
+ Starts the configuration shell interactive loop, that:
+ - Goes to the last current path
+ - Displays the prompt
+ - Waits for user input
+ - Runs user command
+ '''
+ while not self._exit:
+ try:
+ readline.parse_and_bind("tab: complete")
+ readline.set_completer(self._complete)
+ cmdline = six.moves.input(self._get_prompt()).strip()
+ except EOFError:
+ self.con.raw_write('exit\n')
+ cmdline = "exit"
+ self.run_cmdline(cmdline)
+ if self._save_history:
+ try:
+ readline.write_history_file(self._cmd_history)
+ except IOError:
+ self.log.warning(
+ "Cannot write to command history file %s." \
+ % self._cmd_history)
+ self.log.warning(
+ "Saving command history has been disabled!")
+ self._save_history = False
+
+ def _parse_cmdline(self, line):
+ '''
+ Parses the command line entered by the user. This is a wrapper around
+ the actual pyparsing parser that pre-chews the result trees to
+ cleanly extract the tokens we care for (parameters, path, command).
+ @param line: The command line to parse.
+ @type line: str
+ @return: (result_trees, path, command, pparams, kparams),
+ pparams being positional parameters and kparams the keyword=value.
+ @rtype: (pyparsing.ParseResults, str, str, list, dict)
+ '''
+ self.log.debug("Parsing commandline.")
+ path = ''
+ command = ''
+ pparams = []
+ kparams = {}
+
+ parse_results = self._parser.parseString(line)
+ if isinstance(parse_results.path, ParseResults):
+ path = parse_results.path.value
+ if isinstance(parse_results.command, ParseResults):
+ command = parse_results.command.value
+ if isinstance(parse_results.pparams, ParseResults):
+ pparams = [pparam.value for pparam in parse_results.pparams]
+ if isinstance(parse_results.kparams, ParseResults):
+ kparams = dict([kparam.value for kparam in parse_results.kparams])
+
+ self.log.debug("Parse gave path='%s' command='%s' " % (path, command)
+ + "pparams=%s " % str(pparams)
+ + "kparams=%s" % str(kparams))
+ return (parse_results, path, command, pparams, kparams)
+
+ def _execute_command(self, path, command, pparams, kparams):
+ '''
+ Calls the target node to execute a command.
+ Behavior depends on the target node command's result:
+ - An 'EXIT' string will trigger shell exit.
+ - None will do nothing.
+ - A ConfigNode object will trigger a current_node change.
+ @param path: Path of the target node.
+ @type path: str
+ @param command: The command to call.
+ @type command: str
+ @param pparams: The positional parameters to use.
+ @type pparams: list
+ @param kparams: The keyword=value parameters to use.
+ @type kparams: dict
+ '''
+ if path.endswith('*'):
+ path = path.rstrip('*')
+ iterall = True
+ else:
+ iterall = False
+
+ if not path:
+ path = '.'
+
+ if not command:
+ if iterall:
+ command = 'ls'
+ else:
+ command = 'cd'
+ pparams = ['.']
+
+ try:
+ target = self._current_node.get_node(path)
+ except ValueError as msg:
+ raise ExecutionError(str(msg))
+
+ result = None
+ if not iterall:
+ targets = [target]
+ else:
+ targets = target.children
+ for target in targets:
+ if iterall:
+ self.con.display("[%s]" % target.path)
+ result = target.execute_command(command, pparams, kparams)
+ self.log.debug("Command execution returned %r" % result)
+ if isinstance(result, ConfigNode):
+ self._current_node = result
+ elif result == 'EXIT':
+ self._exit = True
+ elif result is not None:
+ raise ExecutionError("Unexpected result: %r" % result)
+
+ # Public methods
+
+ def run_cmdline(self, cmdline):
+ '''
+ Runs the specified command. Global commands are checked first,
+ then local commands from the current node.
+
+ Command syntax is:
+ [PATH] COMMAND [POSITIONAL_PARAMETER]+ [PARAMETER=VALUE]+
+
+ @param cmdline: The command line to run
+ @type cmdline: str
+ '''
+ if cmdline:
+ self.log.debug("Running command line '%s'." % cmdline)
+ path, command, pparams, kparams = self._parse_cmdline(cmdline)[1:]
+ self._execute_command(path, command, pparams, kparams)
+
+ def run_script(self, script_path, exit_on_error=True):
+ '''
+ Runs the script located at script_path.
+ Script runs always start from the root context.
+ @param script_path: File path of the script to run
+ @type script_path: str
+ @param exit_on_error: If True, stops the run if an error occurs
+ @type exit_on_error: bool
+ '''
+ try:
+ script_fd = open(script_path, 'r')
+ self.run_stdin(script_fd, exit_on_error)
+ except IOError as msg:
+ raise IOError(msg)
+ finally:
+ script_fd.close()
+
+ def run_stdin(self, file_descriptor=sys.stdin, exit_on_error=True):
+ '''
+ Reads commands to be run from a file descriptor, stdin by default.
+ The run always starts from the root context.
+ @param file_descriptor: The file descriptor to read commands from
+ @type file_descriptor: file object
+ @param exit_on_error: If True, stops the run if an error occurs
+ @type exit_on_error: bool
+ '''
+ self._current_node = self._root_node
+ for cmdline in file_descriptor:
+ try:
+ self.run_cmdline(cmdline)
+ except Exception as msg:
+ self.log.error(msg)
+ if exit_on_error is True:
+ raise ExecutionError("Aborting run on error.")
+
+ self.log.exception("Keep running after an error.")
+
+ def run_interactive(self):
+ '''
+ Starts interactive CLI mode.
+ '''
+ history = self.prefs['path_history']
+ index = self.prefs['path_history_index']
+ if history and index:
+ if index < len(history):
+ try:
+ target = self._root_node.get_node(history[index])
+ except ValueError:
+ self._current_node = self._root_node
+ else:
+ self._current_node = target
+
+ while True:
+ try:
+ old_completer = readline.get_completer()
+ self._cli_loop()
+ break
+ except KeyboardInterrupt:
+ self.con.raw_write('\n')
+ finally:
+ readline.set_completer(old_completer)
+
+ def attach_root_node(self, root_node):
+ '''
+ @param root_node: The root ConfigNode object
+ @type root_node: ConfigNode
+ '''
+ self._current_node = root_node
+ self._root_node = root_node
diff --git a/configshell_fb b/configshell_fb
new file mode 120000
index 0000000..76ca383
--- /dev/null
+++ b/configshell_fb
@@ -0,0 +1 @@
+configshell \ No newline at end of file
diff --git a/debian/changelog b/debian/changelog
new file mode 100644
index 0000000..fbfb22c
--- /dev/null
+++ b/debian/changelog
@@ -0,0 +1,6 @@
+python-configshell-fb (1.1.20-1) unstable; urgency=medium
+
+ [ Christophe Vu-Brugier ]
+ * Initial release. (Closes: #838861)
+
+ -- Christophe Vu-Brugier <[email protected]> Thu, 22 Sep 2016 22:29:14 +0200
diff --git a/debian/compat b/debian/compat
new file mode 100644
index 0000000..ec63514
--- /dev/null
+++ b/debian/compat
@@ -0,0 +1 @@
+9
diff --git a/debian/control b/debian/control
new file mode 100644
index 0000000..8212b70
--- /dev/null
+++ b/debian/control
@@ -0,0 +1,82 @@
+Source: python-configshell-fb
+Section: python
+Priority: optional
+Maintainer: Debian LIO Target Packagers <[email protected]>
+Uploaders: Christophe Vu-Brugier <[email protected]>,
+ Ritesh Raj Sarraf <[email protected]>,
+ Christian Seiler <[email protected]>
+Build-Depends: debhelper (>= 9),
+ dh-python,
+ python-all,
+ python-epydoc,
+ python-pyparsing,
+ python-setuptools,
+ python-six,
+ python3-all,
+ python3-pyparsing,
+ python3-setuptools,
+ python3-six
+Standards-Version: 3.9.8
+Homepage: https://github.com/open-iscsi/configshell-fb
+Vcs-Git: https://anonscm.debian.org/git/linux-target/python-configshell-fb.git -b debian/master
+Vcs-Browser: https://anonscm.debian.org/cgit/linux-target/python-configshell-fb.git
+
+Package: python-configshell-fb
+Architecture: all
+Depends: ${misc:Depends},
+ ${python:Depends},
+ python-pyparsing,
+ python-six,
+ python-urwid,
+Suggests: python-configshell-fb-doc
+Conflicts: python-configshell
+Description: Python library for building configuration shells - Python 2
+ The configshell-fb package is a Python library that provides a
+ framework for building simple but nice CLI-based applications.
+ .
+ The configshell-fb package is a fork of the "configshell" code
+ written by RisingTide Systems. The "-fb" differentiates between the
+ original and this version. Please ensure to use either all "fb"
+ versions of the targetcli components -- targetcli, rtslib, and
+ configshell, or stick with all non-fb versions, since they are no
+ longer strictly compatible.
+ .
+ This package contains the Python 2 module.
+
+Package: python3-configshell-fb
+Architecture: all
+Depends: ${misc:Depends},
+ ${python3:Depends},
+ python3-pyparsing,
+ python3-six,
+ python3-urwid,
+Suggests: python-configshell-fb-doc
+Description: Python library for building configuration shells - Python 3
+ The configshell-fb package is a Python library that provides a
+ framework for building simple but nice CLI-based applications.
+ .
+ The configshell-fb package is a fork of the "configshell" code
+ written by RisingTide Systems. The "-fb" differentiates between the
+ original and this version. Please ensure to use either all "fb"
+ versions of the targetcli components -- targetcli, rtslib, and
+ configshell, or stick with all non-fb versions, since they are no
+ longer strictly compatible.
+ .
+ This package contains the Python 3 module.
+
+Package: python-configshell-fb-doc
+Section: doc
+Architecture: all
+Depends: ${misc:Depends}
+Description: Python library for building configuration shells - doc
+ The configshell-fb package is a Python library that provides a
+ framework for building simple but nice CLI-based applications.
+ .
+ The configshell-fb package is a fork of the "configshell" code
+ written by RisingTide Systems. The "-fb" differentiates between the
+ original and this version. Please ensure to use either all "fb"
+ versions of the targetcli components -- targetcli, rtslib, and
+ configshell, or stick with all non-fb versions, since they are no
+ longer strictly compatible.
+ .
+ This package contains the documentation.
diff --git a/debian/copyright b/debian/copyright
new file mode 100644
index 0000000..3ed6374
--- /dev/null
+++ b/debian/copyright
@@ -0,0 +1,38 @@
+Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
+Upstream-Name: configshell-fb
+Source: https://github.com/open-iscsi/configshell-fb
+
+Files: *
+Copyright: 2010-2011 RisingTide Systems, LLC.
+ 2010-2013 Jerome Martin <[email protected]>
+ 2011-2013 Datera, Inc.
+ 2011-2016 Andy Grover <[email protected]>
+ 2013-2016 Christophe Vu-Brugier <[email protected]>
+License: Apache-2.0
+
+Files: debian/*
+Comment: The original Debianization took place upstream.
+Copyright: 2010-2011 RisingTide Systems, LLC.
+ 2010-2013 Jerome Martin <[email protected]>
+ 2011-2013 Datera, Inc.
+ 2011-2016 Andy Grover <[email protected]>
+ 2013-2016 Christophe Vu-Brugier <[email protected]>
+ 2016 Ritesh Raj Sarraf <[email protected]>
+ 2016 Christian Seiler <[email protected]>
+License: Apache-2.0
+
+License: Apache-2.0
+ 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
+ .
+ https://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.
+ .
+ On Debian systems, the complete text of the Apache License 2.0 can
+ be found in "/usr/share/common-licenses/Apache-2.0"
diff --git a/debian/gbp.conf b/debian/gbp.conf
new file mode 100644
index 0000000..b41ee25
--- /dev/null
+++ b/debian/gbp.conf
@@ -0,0 +1,12 @@
+[DEFAULT]
+pristine-tar = True
+color = auto
+upstream-branch = upstream/master
+debian-branch = debian/master
+
+[import-orig]
+dch = True
+
+[dch]
+meta = True
+multimaint-merge = True
diff --git a/debian/python-configshell-fb-doc.doc-base b/debian/python-configshell-fb-doc.doc-base
new file mode 100644
index 0000000..f821c86
--- /dev/null
+++ b/debian/python-configshell-fb-doc.doc-base
@@ -0,0 +1,9 @@
+Document: python-configshell-fb
+Title: python-configshell documentation
+Author: Jerome Martin <[email protected]>
+Abstract: configshell is a library which is used to create cli interfaces
+Section: Programming/Python
+
+Format: HTML
+Index: /usr/share/doc/python-configshell-fb-doc/html/index.html
+Files: /usr/share/doc/python-configshell-fb-doc/html/*.html
diff --git a/debian/python-configshell-fb.install b/debian/python-configshell-fb.install
new file mode 100644
index 0000000..b2cc136
--- /dev/null
+++ b/debian/python-configshell-fb.install
@@ -0,0 +1 @@
+usr/lib/python2*
diff --git a/debian/python3-configshell-fb.install b/debian/python3-configshell-fb.install
new file mode 100644
index 0000000..4606faa
--- /dev/null
+++ b/