summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorNicholas Bellinger <nab@risingtidesystems.com>2011-05-04 21:14:18 +0000
committerNicholas Bellinger <nab@risingtidesystems.com>2011-05-04 21:14:18 +0000
commitdb0c7fd76abc5fcc929b039f2d49f1a38f947968 (patch)
treee2d8b8535610d8e6d2c9d1b305230be376d15887
downloadtargetcli-db0c7fd76abc5fcc929b039f2d49f1a38f947968.tar.gz
initial rtsadmin commit1.99
Signed-off-by: Nicholas Bellinger <nab@risingtidesystems.com>
-rw-r--r--COPYING661
-rw-r--r--Makefile87
-rw-r--r--README22
-rwxr-xr-xbin/clean30
-rwxr-xr-xbin/gen_changelog45
-rwxr-xr-xbin/gen_changelog_cleanup20
-rwxr-xr-xbin/gendoc101
-rwxr-xr-xbin/gendoc.py80
-rw-r--r--debian/README.Debian13
-rw-r--r--debian/compat1
-rw-r--r--debian/control25
-rw-r--r--debian/copyright13
-rw-r--r--debian/pyversions1
-rw-r--r--debian/rtsadmin-doc.docs6
-rw-r--r--debian/rtsadmin.dirs1
-rw-r--r--debian/rtsadmin.docs2
-rw-r--r--debian/rtsadmin.install2
-rwxr-xr-xdebian/rtsadmin.postinst17
-rwxr-xr-xdebian/rtsadmin.preinst3
-rwxr-xr-xdebian/rtsadmin.prerm8
-rwxr-xr-xdebian/rules56
-rw-r--r--rtsadmin-doc/html4css1.css273
-rw-r--r--rtsadmin-doc/rtslogo.eps281
-rw-r--r--rtsadmin-doc/rtslogo.pngbin0 -> 8413 bytes
-rw-r--r--rtsadmin-doc/voidspace.css137
-rw-r--r--rtsadmin/__init__.py24
-rw-r--r--rtsadmin/ui_backstore.py482
-rw-r--r--rtsadmin/ui_node.py164
-rw-r--r--rtsadmin/ui_root.py78
-rw-r--r--rtsadmin/ui_target.py846
-rwxr-xr-xscripts/rtsadmin61
-rwxr-xr-xsetup.py43
32 files changed, 3583 insertions, 0 deletions
diff --git a/COPYING b/COPYING
new file mode 100644
index 0000000..dba13ed
--- /dev/null
+++ b/COPYING
@@ -0,0 +1,661 @@
+ GNU AFFERO GENERAL PUBLIC LICENSE
+ Version 3, 19 November 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+ Preamble
+
+ The GNU Affero General Public License is a free, copyleft license for
+software and other kinds of works, specifically designed to ensure
+cooperation with the community in the case of network server software.
+
+ The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works. By contrast,
+our General Public Licenses are intended to guarantee your freedom to
+share and change all versions of a program--to make sure it remains free
+software for all its users.
+
+ When we speak of free software, we are referring to freedom, not
+price. Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+them if you wish), that you receive source code or can get it if you
+want it, that you can change the software or use pieces of it in new
+free programs, and that you know you can do these things.
+
+ Developers that use our General Public Licenses protect your rights
+with two steps: (1) assert copyright on the software, and (2) offer
+you this License which gives you legal permission to copy, distribute
+and/or modify the software.
+
+ A secondary benefit of defending all users' freedom is that
+improvements made in alternate versions of the program, if they
+receive widespread use, become available for other developers to
+incorporate. Many developers of free software are heartened and
+encouraged by the resulting cooperation. However, in the case of
+software used on network servers, this result may fail to come about.
+The GNU General Public License permits making a modified version and
+letting the public access it on a server without ever releasing its
+source code to the public.
+
+ The GNU Affero General Public License is designed specifically to
+ensure that, in such cases, the modified source code becomes available
+to the community. It requires the operator of a network server to
+provide the source code of the modified version running there to the
+users of that server. Therefore, public use of a modified version, on
+a publicly accessible server, gives the public access to the source
+code of the modified version.
+
+ An older license, called the Affero General Public License and
+published by Affero, was designed to accomplish similar goals. This is
+a different license, not a version of the Affero GPL, but Affero has
+released a new version of the Affero GPL which permits relicensing under
+this license.
+
+ The precise terms and conditions for copying, distribution and
+modification follow.
+
+ TERMS AND CONDITIONS
+
+ 0. Definitions.
+
+ "This License" refers to version 3 of the GNU Affero General Public License.
+
+ "Copyright" also means copyright-like laws that apply to other kinds of
+works, such as semiconductor masks.
+
+ "The Program" refers to any copyrightable work licensed under this
+License. Each licensee is addressed as "you". "Licensees" and
+"recipients" may be individuals or organizations.
+
+ To "modify" a work means to copy from or adapt all or part of the work
+in a fashion requiring copyright permission, other than the making of an
+exact copy. The resulting work is called a "modified version" of the
+earlier work or a work "based on" the earlier work.
+
+ A "covered work" means either the unmodified Program or a work based
+on the Program.
+
+ To "propagate" a work means to do anything with it that, without
+permission, would make you directly or secondarily liable for
+infringement under applicable copyright law, except executing it on a
+computer or modifying a private copy. Propagation includes copying,
+distribution (with or without modification), making available to the
+public, and in some countries other activities as well.
+
+ To "convey" a work means any kind of propagation that enables other
+parties to make or receive copies. Mere interaction with a user through
+a computer network, with no transfer of a copy, is not conveying.
+
+ An interactive user interface displays "Appropriate Legal Notices"
+to the extent that it includes a convenient and prominently visible
+feature that (1) displays an appropriate copyright notice, and (2)
+tells the user that there is no warranty for the work (except to the
+extent that warranties are provided), that licensees may convey the
+work under this License, and how to view a copy of this License. If
+the interface presents a list of user commands or options, such as a
+menu, a prominent item in the list meets this criterion.
+
+ 1. Source Code.
+
+ The "source code" for a work means the preferred form of the work
+for making modifications to it. "Object code" means any non-source
+form of a work.
+
+ A "Standard Interface" means an interface that either is an official
+standard defined by a recognized standards body, or, in the case of
+interfaces specified for a particular programming language, one that
+is widely used among developers working in that language.
+
+ The "System Libraries" of an executable work include anything, other
+than the work as a whole, that (a) is included in the normal form of
+packaging a Major Component, but which is not part of that Major
+Component, and (b) serves only to enable use of the work with that
+Major Component, or to implement a Standard Interface for which an
+implementation is available to the public in source code form. A
+"Major Component", in this context, means a major essential component
+(kernel, window system, and so on) of the specific operating system
+(if any) on which the executable work runs, or a compiler used to
+produce the work, or an object code interpreter used to run it.
+
+ The "Corresponding Source" for a work in object code form means all
+the source code needed to generate, install, and (for an executable
+work) run the object code and to modify the work, including scripts to
+control those activities. However, it does not include the work's
+System Libraries, or general-purpose tools or generally available free
+programs which are used unmodified in performing those activities but
+which are not part of the work. For example, Corresponding Source
+includes interface definition files associated with source files for
+the work, and the source code for shared libraries and dynamically
+linked subprograms that the work is specifically designed to require,
+such as by intimate data communication or control flow between those
+subprograms and other parts of the work.
+
+ The Corresponding Source need not include anything that users
+can regenerate automatically from other parts of the Corresponding
+Source.
+
+ The Corresponding Source for a work in source code form is that
+same work.
+
+ 2. Basic Permissions.
+
+ All rights granted under this License are granted for the term of
+copyright on the Program, and are irrevocable provided the stated
+conditions are met. This License explicitly affirms your unlimited
+permission to run the unmodified Program. The output from running a
+covered work is covered by this License only if the output, given its
+content, constitutes a covered work. This License acknowledges your
+rights of fair use or other equivalent, as provided by copyright law.
+
+ You may make, run and propagate covered works that you do not
+convey, without conditions so long as your license otherwise remains
+in force. You may convey covered works to others for the sole purpose
+of having them make modifications exclusively for you, or provide you
+with facilities for running those works, provided that you comply with
+the terms of this License in conveying all material for which you do
+not control copyright. Those thus making or running the covered works
+for you must do so exclusively on your behalf, under your direction
+and control, on terms that prohibit them from making any copies of
+your copyrighted material outside their relationship with you.
+
+ Conveying under any other circumstances is permitted solely under
+the conditions stated below. Sublicensing is not allowed; section 10
+makes it unnecessary.
+
+ 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+ No covered work shall be deemed part of an effective technological
+measure under any applicable law fulfilling obligations under article
+11 of the WIPO copyright treaty adopted on 20 December 1996, or
+similar laws prohibiting or restricting circumvention of such
+measures.
+
+ When you convey a covered work, you waive any legal power to forbid
+circumvention of technological measures to the extent such circumvention
+is effected by exercising rights under this License with respect to
+the covered work, and you disclaim any intention to limit operation or
+modification of the work as a means of enforcing, against the work's
+users, your or third parties' legal rights to forbid circumvention of
+technological measures.
+
+ 4. Conveying Verbatim Copies.
+
+ You may convey verbatim copies of the Program's source code as you
+receive it, in any medium, provided that you conspicuously and
+appropriately publish on each copy an appropriate copyright notice;
+keep intact all notices stating that this License and any
+non-permissive terms added in accord with section 7 apply to the code;
+keep intact all notices of the absence of any warranty; and give all
+recipients a copy of this License along with the Program.
+
+ You may charge any price or no price for each copy that you convey,
+and you may offer support or warranty protection for a fee.
+
+ 5. Conveying Modified Source Versions.
+
+ You may convey a work based on the Program, or the modifications to
+produce it from the Program, in the form of source code under the
+terms of section 4, provided that you also meet all of these conditions:
+
+ a) The work must carry prominent notices stating that you modified
+ it, and giving a relevant date.
+
+ b) The work must carry prominent notices stating that it is
+ released under this License and any conditions added under section
+ 7. This requirement modifies the requirement in section 4 to
+ "keep intact all notices".
+
+ c) You must license the entire work, as a whole, under this
+ License to anyone who comes into possession of a copy. This
+ License will therefore apply, along with any applicable section 7
+ additional terms, to the whole of the work, and all its parts,
+ regardless of how they are packaged. This License gives no
+ permission to license the work in any other way, but it does not
+ invalidate such permission if you have separately received it.
+
+ d) If the work has interactive user interfaces, each must display
+ Appropriate Legal Notices; however, if the Program has interactive
+ interfaces that do not display Appropriate Legal Notices, your
+ work need not make them do so.
+
+ A compilation of a covered work with other separate and independent
+works, which are not by their nature extensions of the covered work,
+and which are not combined with it such as to form a larger program,
+in or on a volume of a storage or distribution medium, is called an
+"aggregate" if the compilation and its resulting copyright are not
+used to limit the access or legal rights of the compilation's users
+beyond what the individual works permit. Inclusion of a covered work
+in an aggregate does not cause this License to apply to the other
+parts of the aggregate.
+
+ 6. Conveying Non-Source Forms.
+
+ You may convey a covered work in object code form under the terms
+of sections 4 and 5, provided that you also convey the
+machine-readable Corresponding Source under the terms of this License,
+in one of these ways:
+
+ a) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by the
+ Corresponding Source fixed on a durable physical medium
+ customarily used for software interchange.
+
+ b) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by a
+ written offer, valid for at least three years and valid for as
+ long as you offer spare parts or customer support for that product
+ model, to give anyone who possesses the object code either (1) a
+ copy of the Corresponding Source for all the software in the
+ product that is covered by this License, on a durable physical
+ medium customarily used for software interchange, for a price no
+ more than your reasonable cost of physically performing this
+ conveying of source, or (2) access to copy the
+ Corresponding Source from a network server at no charge.
+
+ c) Convey individual copies of the object code with a copy of the
+ written offer to provide the Corresponding Source. This
+ alternative is allowed only occasionally and noncommercially, and
+ only if you received the object code with such an offer, in accord
+ with subsection 6b.
+
+ d) Convey the object code by offering access from a designated
+ place (gratis or for a charge), and offer equivalent access to the
+ Corresponding Source in the same way through the same place at no
+ further charge. You need not require recipients to copy the
+ Corresponding Source along with the object code. If the place to
+ copy the object code is a network server, the Corresponding Source
+ may be on a different server (operated by you or a third party)
+ that supports equivalent copying facilities, provided you maintain
+ clear directions next to the object code saying where to find the
+ Corresponding Source. Regardless of what server hosts the
+ Corresponding Source, you remain obligated to ensure that it is
+ available for as long as needed to satisfy these requirements.
+
+ e) Convey the object code using peer-to-peer transmission, provided
+ you inform other peers where the object code and Corresponding
+ Source of the work are being offered to the general public at no
+ charge under subsection 6d.
+
+ A separable portion of the object code, whose source code is excluded
+from the Corresponding Source as a System Library, need not be
+included in conveying the object code work.
+
+ A "User Product" is either (1) a "consumer product", which means any
+tangible personal property which is normally used for personal, family,
+or household purposes, or (2) anything designed or sold for incorporation
+into a dwelling. In determining whether a product is a consumer product,
+doubtful cases shall be resolved in favor of coverage. For a particular
+product received by a particular user, "normally used" refers to a
+typical or common use of that class of product, regardless of the status
+of the particular user or of the way in which the particular user
+actually uses, or expects or is expected to use, the product. A product
+is a consumer product regardless of whether the product has substantial
+commercial, industrial or non-consumer uses, unless such uses represent
+the only significant mode of use of the product.
+
+ "Installation Information" for a User Product means any methods,
+procedures, authorization keys, or other information required to install
+and execute modified versions of a covered work in that User Product from
+a modified version of its Corresponding Source. The information must
+suffice to ensure that the continued functioning of the modified object
+code is in no case prevented or interfered with solely because
+modification has been made.
+
+ If you convey an object code work under this section in, or with, or
+specifically for use in, a User Product, and the conveying occurs as
+part of a transaction in which the right of possession and use of the
+User Product is transferred to the recipient in perpetuity or for a
+fixed term (regardless of how the transaction is characterized), the
+Corresponding Source conveyed under this section must be accompanied
+by the Installation Information. But this requirement does not apply
+if neither you nor any third party retains the ability to install
+modified object code on the User Product (for example, the work has
+been installed in ROM).
+
+ The requirement to provide Installation Information does not include a
+requirement to continue to provide support service, warranty, or updates
+for a work that has been modified or installed by the recipient, or for
+the User Product in which it has been modified or installed. Access to a
+network may be denied when the modification itself materially and
+adversely affects the operation of the network or violates the rules and
+protocols for communication across the network.
+
+ Corresponding Source conveyed, and Installation Information provided,
+in accord with this section must be in a format that is publicly
+documented (and with an implementation available to the public in
+source code form), and must require no special password or key for
+unpacking, reading or copying.
+
+ 7. Additional Terms.
+
+ "Additional permissions" are terms that supplement the terms of this
+License by making exceptions from one or more of its conditions.
+Additional permissions that are applicable to the entire Program shall
+be treated as though they were included in this License, to the extent
+that they are valid under applicable law. If additional permissions
+apply only to part of the Program, that part may be used separately
+under those permissions, but the entire Program remains governed by
+this License without regard to the additional permissions.
+
+ When you convey a copy of a covered work, you may at your option
+remove any additional permissions from that copy, or from any part of
+it. (Additional permissions may be written to require their own
+removal in certain cases when you modify the work.) You may place
+additional permissions on material, added by you to a covered work,
+for which you have or can give appropriate copyright permission.
+
+ Notwithstanding any other provision of this License, for material you
+add to a covered work, you may (if authorized by the copyright holders of
+that material) supplement the terms of this License with terms:
+
+ a) Disclaiming warranty or limiting liability differently from the
+ terms of sections 15 and 16 of this License; or
+
+ b) Requiring preservation of specified reasonable legal notices or
+ author attributions in that material or in the Appropriate Legal
+ Notices displayed by works containing it; or
+
+ c) Prohibiting misrepresentation of the origin of that material, or
+ requiring that modified versions of such material be marked in
+ reasonable ways as different from the original version; or
+
+ d) Limiting the use for publicity purposes of names of licensors or
+ authors of the material; or
+
+ e) Declining to grant rights under trademark law for use of some
+ trade names, trademarks, or service marks; or
+
+ f) Requiring indemnification of licensors and authors of that
+ material by anyone who conveys the material (or modified versions of
+ it) with contractual assumptions of liability to the recipient, for
+ any liability that these contractual assumptions directly impose on
+ those licensors and authors.
+
+ All other non-permissive additional terms are considered "further
+restrictions" within the meaning of section 10. If the Program as you
+received it, or any part of it, contains a notice stating that it is
+governed by this License along with a term that is a further
+restriction, you may remove that term. If a license document contains
+a further restriction but permits relicensing or conveying under this
+License, you may add to a covered work material governed by the terms
+of that license document, provided that the further restriction does
+not survive such relicensing or conveying.
+
+ If you add terms to a covered work in accord with this section, you
+must place, in the relevant source files, a statement of the
+additional terms that apply to those files, or a notice indicating
+where to find the applicable terms.
+
+ Additional terms, permissive or non-permissive, may be stated in the
+form of a separately written license, or stated as exceptions;
+the above requirements apply either way.
+
+ 8. Termination.
+
+ You may not propagate or modify a covered work except as expressly
+provided under this License. Any attempt otherwise to propagate or
+modify it is void, and will automatically terminate your rights under
+this License (including any patent licenses granted under the third
+paragraph of section 11).
+
+ However, if you cease all violation of this License, then your
+license from a particular copyright holder is reinstated (a)
+provisionally, unless and until the copyright holder explicitly and
+finally terminates your license, and (b) permanently, if the copyright
+holder fails to notify you of the violation by some reasonable means
+prior to 60 days after the cessation.
+
+ Moreover, your license from a particular copyright holder is
+reinstated permanently if the copyright holder notifies you of the
+violation by some reasonable means, this is the first time you have
+received notice of violation of this License (for any work) from that
+copyright holder, and you cure the violation prior to 30 days after
+your receipt of the notice.
+
+ Termination of your rights under this section does not terminate the
+licenses of parties who have received copies or rights from you under
+this License. If your rights have been terminated and not permanently
+reinstated, you do not qualify to receive new licenses for the same
+material under section 10.
+
+ 9. Acceptance Not Required for Having Copies.
+
+ You are not required to accept this License in order to receive or
+run a copy of the Program. Ancillary propagation of a covered work
+occurring solely as a consequence of using peer-to-peer transmission
+to receive a copy likewise does not require acceptance. However,
+nothing other than this License grants you permission to propagate or
+modify any covered work. These actions infringe copyright if you do
+not accept this License. Therefore, by modifying or propagating a
+covered work, you indicate your acceptance of this License to do so.
+
+ 10. Automatic Licensing of Downstream Recipients.
+
+ Each time you convey a covered work, the recipient automatically
+receives a license from the original licensors, to run, modify and
+propagate that work, subject to this License. You are not responsible
+for enforcing compliance by third parties with this License.
+
+ An "entity transaction" is a transaction transferring control of an
+organization, or substantially all assets of one, or subdividing an
+organization, or merging organizations. If propagation of a covered
+work results from an entity transaction, each party to that
+transaction who receives a copy of the work also receives whatever
+licenses to the work the party's predecessor in interest had or could
+give under the previous paragraph, plus a right to possession of the
+Corresponding Source of the work from the predecessor in interest, if
+the predecessor has it or can get it with reasonable efforts.
+
+ You may not impose any further restrictions on the exercise of the
+rights granted or affirmed under this License. For example, you may
+not impose a license fee, royalty, or other charge for exercise of
+rights granted under this License, and you may not initiate litigation
+(including a cross-claim or counterclaim in a lawsuit) alleging that
+any patent claim is infringed by making, using, selling, offering for
+sale, or importing the Program or any portion of it.
+
+ 11. Patents.
+
+ A "contributor" is a copyright holder who authorizes use under this
+License of the Program or a work on which the Program is based. The
+work thus licensed is called the contributor's "contributor version".
+
+ A contributor's "essential patent claims" are all patent claims
+owned or controlled by the contributor, whether already acquired or
+hereafter acquired, that would be infringed by some manner, permitted
+by this License, of making, using, or selling its contributor version,
+but do not include claims that would be infringed only as a
+consequence of further modification of the contributor version. For
+purposes of this definition, "control" includes the right to grant
+patent sublicenses in a manner consistent with the requirements of
+this License.
+
+ Each contributor grants you a non-exclusive, worldwide, royalty-free
+patent license under the contributor's essential patent claims, to
+make, use, sell, offer for sale, import and otherwise run, modify and
+propagate the contents of its contributor version.
+
+ In the following three paragraphs, a "patent license" is any express
+agreement or commitment, however denominated, not to enforce a patent
+(such as an express permission to practice a patent or covenant not to
+sue for patent infringement). To "grant" such a patent license to a
+party means to make such an agreement or commitment not to enforce a
+patent against the party.
+
+ If you convey a covered work, knowingly relying on a patent license,
+and the Corresponding Source of the work is not available for anyone
+to copy, free of charge and under the terms of this License, through a
+publicly available network server or other readily accessible means,
+then you must either (1) cause the Corresponding Source to be so
+available, or (2) arrange to deprive yourself of the benefit of the
+patent license for this particular work, or (3) arrange, in a manner
+consistent with the requirements of this License, to extend the patent
+license to downstream recipients. "Knowingly relying" means you have
+actual knowledge that, but for the patent license, your conveying the
+covered work in a country, or your recipient's use of the covered work
+in a country, would infringe one or more identifiable patents in that
+country that you have reason to believe are valid.
+
+ If, pursuant to or in connection with a single transaction or
+arrangement, you convey, or propagate by procuring conveyance of, a
+covered work, and grant a patent license to some of the parties
+receiving the covered work authorizing them to use, propagate, modify
+or convey a specific copy of the covered work, then the patent license
+you grant is automatically extended to all recipients of the covered
+work and works based on it.
+
+ A patent license is "discriminatory" if it does not include within
+the scope of its coverage, prohibits the exercise of, or is
+conditioned on the non-exercise of one or more of the rights that are
+specifically granted under this License. You may not convey a covered
+work if you are a party to an arrangement with a third party that is
+in the business of distributing software, under which you make payment
+to the third party based on the extent of your activity of conveying
+the work, and under which the third party grants, to any of the
+parties who would receive the covered work from you, a discriminatory
+patent license (a) in connection with copies of the covered work
+conveyed by you (or copies made from those copies), or (b) primarily
+for and in connection with specific products or compilations that
+contain the covered work, unless you entered into that arrangement,
+or that patent license was granted, prior to 28 March 2007.
+
+ Nothing in this License shall be construed as excluding or limiting
+any implied license or other defenses to infringement that may
+otherwise be available to you under applicable patent law.
+
+ 12. No Surrender of Others' Freedom.
+
+ If conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot convey a
+covered work so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you may
+not convey it at all. For example, if you agree to terms that obligate you
+to collect a royalty for further conveying from those to whom you convey
+the Program, the only way you could satisfy both those terms and this
+License would be to refrain entirely from conveying the Program.
+
+ 13. Remote Network Interaction; Use with the GNU General Public License.
+
+ Notwithstanding any other provision of this License, if you modify the
+Program, your modified version must prominently offer all users
+interacting with it remotely through a computer network (if your version
+supports such interaction) an opportunity to receive the Corresponding
+Source of your version by providing access to the Corresponding Source
+from a network server at no charge, through some standard or customary
+means of facilitating copying of software. This Corresponding Source
+shall include the Corresponding Source for any work covered by version 3
+of the GNU General Public License that is incorporated pursuant to the
+following paragraph.
+
+ Notwithstanding any other provision of this License, you have
+permission to link or combine any covered work with a work licensed
+under version 3 of the GNU General Public License into a single
+combined work, and to convey the resulting work. The terms of this
+License will continue to apply to the part which is the covered work,
+but the work with which it is combined will remain governed by version
+3 of the GNU General Public License.
+
+ 14. Revised Versions of this License.
+
+ The Free Software Foundation may publish revised and/or new versions of
+the GNU Affero General Public License from time to time. Such new versions
+will be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+ Each version is given a distinguishing version number. If the
+Program specifies that a certain numbered version of the GNU Affero General
+Public License "or any later version" applies to it, you have the
+option of following the terms and conditions either of that numbered
+version or of any later version published by the Free Software
+Foundation. If the Program does not specify a version number of the
+GNU Affero General Public License, you may choose any version ever published
+by the Free Software Foundation.
+
+ If the Program specifies that a proxy can decide which future
+versions of the GNU Affero General Public License can be used, that proxy's
+public statement of acceptance of a version permanently authorizes you
+to choose that version for the Program.
+
+ Later license versions may give you additional or different
+permissions. However, no additional obligations are imposed on any
+author or copyright holder as a result of your choosing to follow a
+later version.
+
+ 15. Disclaimer of Warranty.
+
+ THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
+APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
+HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
+OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
+IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
+ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+ 16. Limitation of Liability.
+
+ IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
+THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
+GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
+USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
+DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
+PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
+EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGES.
+
+ 17. Interpretation of Sections 15 and 16.
+
+ If the disclaimer of warranty and limitation of liability provided
+above cannot be given local legal effect according to their terms,
+reviewing courts shall apply local law that most closely approximates
+an absolute waiver of all civil liability in connection with the
+Program, unless a warranty or assumption of liability accompanies a
+copy of the Program in return for a fee.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Programs
+
+ If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+ To do so, attach the following notices to the program. It is safest
+to attach them to the start of each source file to most effectively
+state the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+ <one line to give the program's name and a brief idea of what it does.>
+ Copyright (C) <year> <name of author>
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+Also add information on how to contact you by electronic and paper mail.
+
+ If your software can interact with users remotely through a computer
+network, you should also make sure that it provides a way for users to
+get its source. For example, if your program is a web application, its
+interface could display a "Source" link that leads users to an archive
+of the code. There are many ways you could offer source, and different
+solutions will be better for different programs; see section 13 for the
+specific requirements.
+
+ You should also get your employer (if you work as a programmer) or school,
+if any, to sign a "copyright disclaimer" for the program, if necessary.
+For more information on this, and how to apply and follow the GNU AGPL, see
+<http://www.gnu.org/licenses/>.
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..7cd61f7
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,87 @@
+# This file is part of RTSAdmin Community Edition.
+# Copyright (c) 2011 by RisingTide Systems LLC
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, version 3 (AGPLv3).
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+NAME = rtsadmin
+LIB = /usr/share
+DOC = ${LIB}/doc/
+SETUP = ./setup.py
+CLEAN = ./bin/clean
+GENDOC = ./bin/gendoc
+
+all: usage
+usage:
+ @echo "Usage:"
+ @echo " make clean - Cleanup the local repository"
+ @echo " make packages - Generate the Debian and RPM packages"
+ @echo
+ @echo "Developer targets:"
+ @echo " make doc - Generate the documentation"
+ @echo " make sdist - Build the source tarball"
+ @echo " make bdist - Build the installable tarball"
+ @echo " make install - Install rtsadmin"
+ @echo " make installdocs - Install the documentation"
+
+install:
+ ${SETUP} install
+
+doc:
+ ${GENDOC}
+
+installdocs: doc
+ @test -e ${DOC} || \
+ echo "Could not find ${DOC}; check the makefile variables."
+ @test -e ${DOC}
+ cp -r doc/* ${DOC}/${NAME}/
+
+clean:
+ ${CLEAN}
+ ./bin/gen_changelog_cleanup
+
+packages: clean doc
+ ./bin/gen_changelog
+ dpkg-buildpackage -rfakeroot | tee dpkg-buildpackage.log
+ head -1 debian/changelog | awk '{print $$2}' | tr -d "()\n" > dpkg-buildpackage.version
+ ./bin/gen_changelog_cleanup
+ @test -e dist || mkdir dist
+ mv ../${NAME}_$$(cat dpkg-buildpackage.version).dsc dist
+ mv ../${NAME}_$$(cat dpkg-buildpackage.version)_*.changes dist
+ mv ../${NAME}_$$(cat dpkg-buildpackage.version).tar.gz dist
+ mv ../*${NAME}*$$(cat dpkg-buildpackage.version)*.deb dist
+ @test -e build || mkdir build
+ cd build; alien --scripts -k -g -r ../dist/rtsadmin-doc_$$(cat ../dpkg-buildpackage.version)_all.deb
+ cd build/rtsadmin-doc-*; mkdir usr/share/doc/packages
+ cd build/rtsadmin-doc-*; mv usr/share/doc/rtsadmin-doc usr/share/doc/packages/
+ cd build/rtsadmin-doc-*; perl -pi -e "s,/usr/share/doc/rtsadmin-doc,/usr/share/doc/packages/rtsadmin-doc,g" *.spec
+ cd build/rtsadmin-doc-*; perl -pi -e "s,%%{ARCH},noarch,g" *.spec
+ cd build/rtsadmin-doc-*; perl -pi -e "s,%post,%posttrans,g" *.spec
+ cd build/rtsadmin-doc-*; rpmbuild --buildroot $$PWD -bb *.spec
+ cd build; alien --scripts -k -g -r ../dist/rtsadmin_$$(cat ../dpkg-buildpackage.version)_all.deb; cd ..
+ cd build/rtsadmin*; mkdir usr/share/doc/packages
+ cd build/rtsadmin*; mv usr/share/doc/rtsadmin usr/share/doc/packages/
+ cd build/rtsadmin*; perl -pi -e "s,/usr/share/doc/rtsadmin,/usr/share/doc/packages/rtsadmin,g" *.spec
+ cd build/rtsadmin*; perl -pi -e 's/Group:/Requires: python >= 2.5, python-rtslib\nConflicts: rtsadmin-frozen\nGroup:/g' *.spec
+ cd build/rtsadmin*; perl -pi -e "s,%%{ARCH},noarch,g" *.spec
+ cd build/rtsadmin*; perl -pi -e "s,%post,%posttrans,g" *.spec
+ cd build/rtsadmin*; rpmbuild --buildroot $$PWD -bb *.spec
+ rm -rf build/rtsadmin*$$(cat dpkg-buildpackage.version)
+ mv build/*.rpm dist
+ rm dpkg-buildpackage.log dpkg-buildpackage.version
+
+sdist: clean doc
+ ${SETUP} sdist
+
+bdist: clean doc
+ ${SETUP} bdist
+
diff --git a/README b/README
new file mode 100644
index 0000000..c92d684
--- /dev/null
+++ b/README
@@ -0,0 +1,22 @@
+# This file is part of RTSAdmin Community Edition.
+# Copyright (c) 2011 by RisingTide Systems LLC
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, version 3 (AGPLv3).
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+RTSAdmin Community Edition is an administration tool for managing RisingTide
+Systems storage targets using the kernel LIO core target and compatible target
+fabric modules. The rtsadmin CLI is built on top of the python configshell CLI
+framework.
+
+To run the CLI from this directory use:
+sudo PYTHONPATH=. ./scripts/rtsadmin
diff --git a/bin/clean b/bin/clean
new file mode 100755
index 0000000..371d0f3
--- /dev/null
+++ b/bin/clean
@@ -0,0 +1,30 @@
+#!/bin/bash
+
+# This file is part of RTSAdmin Community Edition.
+# Copyright (c) 2011 by RisingTide Systems LLC
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, version 3 (AGPLv3).
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+rm -v rtsadmin/*.pyc rtsadmin/*.html 2>/dev/null
+rm -rvf doc 2>/dev/null
+rm -rfv rtsadmin.egg-info MANIFEST build dist 2>/dev/null
+rm -rvf pdf html 2>/dev/null
+rm -rvf debian/tmp 2>/dev/null
+rm -v build-stamp 2>/dev/null
+rm -v dpkg-buildpackage.log dpkg-buildpackage.version 2>/dev/null
+rm -rfv *.rpm warnrtsadmin.txt buildrtsadmin 2>/dev/null
+rm -v debian/*.debhelper.log debian/*.debhelper debian/*.substvars debian/files 2>/dev/null
+rm -vrf debian/rtsadmin-frozen/ debian/rtsadmin-python2.5/ debian/rtsadmin-python2.6/ debian/rtsadmin/ debian/rtsadmin-doc/ 2>/dev/null
+
+echo "Finished cleanup."
+
diff --git a/bin/gen_changelog b/bin/gen_changelog
new file mode 100755
index 0000000..8a25923
--- /dev/null
+++ b/bin/gen_changelog
@@ -0,0 +1,45 @@
+#!/bin/bash
+
+# This file is part of RTSAdmin Community Edition.
+# Copyright (c) 2011 by RisingTide Systems LLC
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, version 3 (AGPLv3).
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+NAME=$(git config --get user.name)
+EMAIL=$(git config --get user.email)
+DATE=$(date +'%a, %d %b %Y %H:%M:%S %z')
+
+SRCPKG=$(cat debian/control | grep ^Source | awk '{print $2}')
+
+LAST_TAG=$(git tag | tail -1)
+if [ -z $LAST_TAG ]; then
+ LAST_TAG='0.0'
+fi
+
+COMMIT=$(git log ${LAST_TAG}..HEAD | head -1 | colrm 1 7 | colrm 8)
+TS=$(date +%Y%m%d%H%M%S)
+
+if [ -z $COMMIT ]; then
+ VERSION="${LAST_TAG}"
+else
+ VERSION="${LAST_TAG}-${TS}.${COMMIT}"
+fi
+
+sed -i "s/__version__ = .*/__version__ = '${VERSION}'/g" ${SRCPKG}/__init__.py
+
+echo "${SRCPKG} (${VERSION}) unstable; urgency=low" > debian/changelog
+echo >> debian/changelog
+echo " * Generated package." >> debian/changelog
+echo >> debian/changelog
+echo " -- ${NAME} <${EMAIL}> ${DATE}" >> debian/changelog
+
diff --git a/bin/gen_changelog_cleanup b/bin/gen_changelog_cleanup
new file mode 100755
index 0000000..3d11deb
--- /dev/null
+++ b/bin/gen_changelog_cleanup
@@ -0,0 +1,20 @@
+#!/bin/bash
+
+# This file is part of RTSAdmin Community Edition.
+# Copyright (c) 2011 by RisingTide Systems LLC
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, version 3 (AGPLv3).
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+SRCPKG=$(cat debian/control | grep ^Source | awk '{print $2}')
+rm -f debian/changelog
+sed -i "s/__version__ = .*/__version__ = 'GIT_VERSION'/g" ${SRCPKG}/__init__.py
diff --git a/bin/gendoc b/bin/gendoc
new file mode 100755
index 0000000..6b50f9c
--- /dev/null
+++ b/bin/gendoc
@@ -0,0 +1,101 @@
+#!/bin/bash
+
+# This file is part of RTSAdmin Community Edition.
+# Copyright (c) 2011 by RisingTide Systems LLC
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, version 3 (AGPLv3).
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+path=$PWD
+
+rm -rf doc &>/dev/null
+mkdir doc
+cp README COPYING doc/
+cp rtsadmin-doc/rtslogo.* doc/
+
+CSS=../rtsadmin-doc/html4css1.css
+#CSS=../rtsadmin-doc/voidspace.css
+RSTTITLE="rtsadmin_reference"
+TITLE=$RSTTITLE
+#MARK="% generated by Docutils"
+MARK="\documentclass.*"
+./bin/gendoc.py > doc/$TITLE.rst
+
+cd doc
+rst2html $TITLE.rst --stylesheet-path $CSS --embed-stylesheet --no-xml-declaration $TITLE.html
+cat $TITLE.rst | grep -v ".. sectnum::" | grep -v "header::" > $TITLE.rst2
+mv $TITLE.rst2 $TITLE.rst
+
+rst2latex $RSTTITLE.rst --toc-entry-backlinks --compound-enumerators --use-verbatim-when-possible --use-latex-toc --section-numbering --documentclass article --documentoptions "10pt,a4paper" --use-latex-toc --use-latex-docinfo --hyperlink-color black $TITLE.tex
+
+perl -pi -e "s/\($MARK\)/\1\n\\\usepackage{fancyhdr}/g" $TITLE.tex
+perl -pi -e "s/\($MARK\)/\1\n\\\usepackage{lastpage}/g" $TITLE.tex
+perl -pi -e "s/\($MARK\)/\1\n\\\usepackage{color}/g" $TITLE.tex
+perl -pi -e "s/\($MARK\)/\1\n\\\usepackage{titlesec}/g" $TITLE.tex
+perl -pi -e "s/\($MARK\)/\1\n\\\pagestyle{fancy}/g" $TITLE.tex
+perl -pi -e "s/\($MARK\)/\1\n\\\\definecolor{rtsblue}{RGB}{79,130,189}/g" $TITLE.tex
+perl -pi -e "s/\($MARK\)/\1\n\\\rhead{\\\includegraphics[height=13pt]{rtslogo.eps}}/g" $TITLE.tex
+perl -pi -e "s/\($MARK\)/\1\n\\\chead{}/g" $TITLE.tex
+perl -pi -e "s/\($MARK\)/\1\n\\\lhead{\\\leftmark}/g" $TITLE.tex
+perl -pi -e "s/\($MARK\)/\1\n\\\renewcommand{\\\headrule}{{\\\color{rtsblue}\\\hrule width\\\headwidth height\\\headrulewidth \\\vskip-\\\headrulewidth}}/g" $TITLE.tex
+perl -pi -e "s/\($MARK\)/\1\n\\\cfoot{}/g" $TITLE.tex
+perl -pi -e "s/\($MARK\)/\1\n\\\lfoot{Copyright \\\copyright2009-2010 by RisingTide Systems LLC}/g" $TITLE.tex
+perl -pi -e "s/\($MARK\)/\1\n\\\rfoot{Page \\\thepage\\\ of\\\ \\\pageref{LastPage}}/g" $TITLE.tex
+perl -pi -e "s/\($MARK\)/\1\n\\\renewcommand{\\\footrule}{{\\\color{rtsblue}\\\hrule width\\\headwidth height\\\headrulewidth \\\vskip-\\\headrulewidth}}/g" $TITLE.tex
+perl -pi -e "s/\($MARK\)/\1\n\\\renewcommand{\\\headrulewidth}{0.4pt}/g" $TITLE.tex
+perl -pi -e "s/\($MARK\)/\1\n\\\renewcommand{\\\footrulewidth}{0.4pt}/g" $TITLE.tex
+perl -pi -e "s/\($MARK\)/\1\n\\\\titleformat{\\\chapter}\[display\]{\\\color{rtsblue}\\\normalfont\\\huge\\\bfseries}{\\\chaptertitlename\\\thechapter}{20pt}{\\\Huge}/g" $TITLE.tex
+perl -pi -e "s/\($MARK\)/\1\n\\\\titleformat{\\\section}{\\\color{rtsblue}\\\normalfont\\\Large\\\bfseries}{\\\thesection}{1em}{}/g" $TITLE.tex
+perl -pi -e "s/\($MARK\)/\1\n\\\\titleformat{\\\subsection}{\\\color{rtsblue}\\\normalfont\\\large\\\bfseries}{\\\thesubsection}{1em}{}/g" $TITLE.tex
+
+# once for generating the formatting
+latex $TITLE.tex $TITLE.dvi
+# once more for the TOC
+latex $TITLE.tex $TITLE.dvi
+# And now get the labels right
+latex $TITLE.tex $TITLE.dvi
+dvipdf $TITLE.dvi $TITLE.pdf
+
+TITLE="rtsadmin_reference_for_print"
+
+rst2latex $RSTTITLE.rst --toc-entry-backlinks --compound-enumerators --use-verbatim-when-possible --use-latex-toc --section-numbering --documentclass book --documentoptions "10pt,a4paper" --use-latex-toc --use-latex-docinfo --hyperlink-color black $TITLE.tex
+
+perl -pi -e "s/\($MARK\)/\1\n\\\usepackage{fancyhdr}/g" $TITLE.tex
+perl -pi -e "s/\($MARK\)/\1\n\\\usepackage{lastpage}/g" $TITLE.tex
+perl -pi -e "s/\($MARK\)/\1\n\\\usepackage{color}/g" $TITLE.tex
+perl -pi -e "s/\($MARK\)/\1\n\\\usepackage{titlesec}/g" $TITLE.tex
+perl -pi -e "s/\($MARK\)/\1\n\\\\definecolor{rtsblue}{RGB}{79,130,189}/g" $TITLE.tex
+perl -pi -e "s/\($MARK\)/\1\n\\\pagestyle{fancy}/g" $TITLE.tex
+perl -pi -e "s/\($MARK\)/\1\n\\\rhead{\\\includegraphics[height=13pt]{rtslogo.eps}}/g" $TITLE.tex
+perl -pi -e "s/\($MARK\)/\1\n\\\chead{}/g" $TITLE.tex
+perl -pi -e "s/\($MARK\)/\1\n\\\lhead{\\\leftmark}/g" $TITLE.tex
+perl -pi -e "s/\($MARK\)/\1\n\\\renewcommand{\\\headrule}{{\\\color{rtsblue}\\\hrule width\\\headwidth height\\\headrulewidth \\\vskip-\\\headrulewidth}}/g" $TITLE.tex
+perl -pi -e "s/\($MARK\)/\1\n\\\cfoot{}/g" $TITLE.tex
+perl -pi -e "s/\($MARK\)/\1\n\\\lfoot{Copyright \\\copyright2009-2010 by RisingTide Systems LLC}/g" $TITLE.tex
+perl -pi -e "s/\($MARK\)/\1\n\\\rfoot{Page \\\thepage\\\ of\\\ \\\pageref{LastPage}}/g" $TITLE.tex
+perl -pi -e "s/\($MARK\)/\1\n\\\renewcommand{\\\footrule}{{\\\color{rtsblue}\\\hrule width\\\headwidth height\\\headrulewidth \\\vskip-\\\headrulewidth}}/g" $TITLE.tex
+perl -pi -e "s/\($MARK\)/\1\n\\\renewcommand{\\\headrulewidth}{0.4pt}/g" $TITLE.tex
+perl -pi -e "s/\($MARK\)/\1\n\\\renewcommand{\\\footrulewidth}{0.4pt}/g" $TITLE.tex
+perl -pi -e "s/\($MARK\)/\1\n\\\\titleformat{\\\chapter}\[display\]{\\\color{rtsblue}\\\normalfont\\\huge\\\bfseries}{\\\chaptertitlename\\\thechapter}{20pt}{\\\Huge}/g" $TITLE.tex
+perl -pi -e "s/\($MARK\)/\1\n\\\\titleformat{\\\section}{\\\color{rtsblue}\\\normalfont\\\Large\\\bfseries}{\\\thesection}{1em}{}/g" $TITLE.tex
+
+# once for generating the formatting
+latex $TITLE.tex $TITLE.dvi
+# once more for the TOC
+latex $TITLE.tex $TITLE.dvi
+# And now get the labels right
+latex $TITLE.tex $TITLE.dvi
+dvipdf $TITLE.dvi $TITLE.pdf
+
+
+cd ..
+
diff --git a/bin/gendoc.py b/bin/gendoc.py
new file mode 100755
index 0000000..680555a
--- /dev/null
+++ b/bin/gendoc.py
@@ -0,0 +1,80 @@
+#!/usr/bin/python
+
+# This file is part of RTSAdmin Community Edition.
+# Copyright (c) 2011 by RisingTide Systems LLC
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, version 3 (AGPLv3).
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import os
+import re
+import sys
+import inspect
+import textwrap
+import datetime
+sys.path.insert(0, os.path.realpath("."))
+import rtsadmin
+
+def doctitle(txt):
+ return "%s\n%s\n%s\n\n" \
+ % ("".ljust(len(txt), "="), txt, "".ljust(len(txt), "="))
+
+def docsubtitle(txt):
+ return "%s\n%s\n%s\n\n" \
+ % ("".ljust(len(txt), "-"), txt, "".ljust(len(txt), "-"))
+
+def title(txt):
+ return "%s\n%s\n\n" % (txt, "".ljust(len(txt), "="))
+
+def subtitle(txt):
+ return "%s\n%s\n\n" % (txt, "".ljust(len(txt), "-"))
+
+def gen_context_reference():
+ string = ""
+ string += "Documentation extract not implemented yet"
+ return string
+
+def gen_help_misc():
+ string = ""
+ string += "Documentation extract not implemented yet"
+ return string
+
+def gen_help_cmd():
+ string = title("Globally Available Commands")
+ string += "Documentation extract not implemented yet"
+ return string
+
+def gen_title():
+ string = ""
+ string += doctitle("The rtsadmin Reference Guide")
+ string += docsubtitle(rtsadmin.__description__)
+ string += ":Date: version %s built on %s\n" \
+ % (str(rtsadmin.__version__),
+ str(datetime.datetime.now().date()))
+ license_fields = rtsadmin.__license__.split(".")
+ string += ":Author: %s. %s.\n" % (license_fields[0], license_fields[1])
+ string += "\n"
+ return string
+
+def gen_toc():
+ return ".. contents:: **Table of Contents**\n\n"
+
+def gen_options():
+ return ".. sectnum::\n\n.. header:: .. image:: rtslogo.png\n\n"
+
+if __name__ == "__main__":
+ print gen_title()
+ print gen_options()
+ print gen_toc()
+ print gen_help_misc()
+ print gen_help_cmd()
+ print gen_context_reference()
diff --git a/debian/README.Debian b/debian/README.Debian
new file mode 100644
index 0000000..374bba0
--- /dev/null
+++ b/debian/README.Debian
@@ -0,0 +1,13 @@
+Copyright (c) 2011 by RisingTide Systems LLC
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU Affero General Public License as
+published by the Free Software Foundation, version 3 (AGPLv3).
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU Affero General Public License for more details.
+
+You should have received a copy of the GNU Affero General Public License
+along with this program. If not, see <http://www.gnu.org/licenses/>.
diff --git a/debian/compat b/debian/compat
new file mode 100644
index 0000000..7ed6ff8
--- /dev/null
+++ b/debian/compat
@@ -0,0 +1 @@
+5
diff --git a/debian/control b/debian/control
new file mode 100644
index 0000000..3bbfb80
--- /dev/null
+++ b/debian/control
@@ -0,0 +1,25 @@
+Source: rtsadmin
+Section: python
+Priority: optional
+Maintainer: Jerome Martin <jxm@risingtidesystems.com>
+Build-Depends: debhelper(>= 5.0.1), python2.6, build-essential, python-dev, python2.6-dev, python-epydoc, zlib1g-dev, python-configshell, upx-ucl
+Standards-Version: 3.8.1
+
+Package: rtsadmin
+Architecture: all
+Depends: python (>= 2.6)|python2.6, python-configshell
+Suggests: rtsadmin-doc
+Conflicts: rtsadmin-frozen
+Description: CLI for the RisingTide Systems target, with complete sources.
+ .
+ This package contains the rtsadmin CLI.
+
+Package: rtsadmin-doc
+Section: doc
+Architecture: all
+Recommends: iceweasel | www-browser
+Description: User manual for the rtsadmin CLI
+ .
+ This package contains the User Manual for rtsadmin, the CLI
+ interface to the RisingTide Systems storage clusters.
+
diff --git a/debian/copyright b/debian/copyright
new file mode 100644
index 0000000..476386c
--- /dev/null
+++ b/debian/copyright
@@ -0,0 +1,13 @@
+This package was originally debianized by Jerome Martin <jxm@risingtidesystems.com>
+on Thu Nov 19 12:00:01 UTC 2009. It is currently maintained by Jerome Martin
+<jxm@risingtidesystems.com>.
+
+Upstream Author: Jerome Martin <jxm@risingtidesystems.com>
+
+Copyright:
+
+ Copyright (c) 2009-2010 by RisingTide Systems LLC.
+ All rights reserved.
+ For licensing information, please contact us.
+ Not for redistribution.
+
diff --git a/debian/pyversions b/debian/pyversions
new file mode 100644
index 0000000..0c043f1
--- /dev/null
+++ b/debian/pyversions
@@ -0,0 +1 @@
+2.6-
diff --git a/debian/rtsadmin-doc.docs b/debian/rtsadmin-doc.docs
new file mode 100644
index 0000000..97c4a78
--- /dev/null
+++ b/debian/rtsadmin-doc.docs
@@ -0,0 +1,6 @@
+doc/README
+doc/COPYING
+doc/rtsadmin_reference.html
+doc/rtsadmin_reference.pdf
+doc/rtsadmin_reference_for_print.pdf
+doc/rtslogo.png
diff --git a/debian/rtsadmin.dirs b/debian/rtsadmin.dirs
new file mode 100644
index 0000000..f430183
--- /dev/null
+++ b/debian/rtsadmin.dirs
@@ -0,0 +1 @@
+usr/share/python-support/
diff --git a/debian/rtsadmin.docs b/debian/rtsadmin.docs
new file mode 100644
index 0000000..1f562b3
--- /dev/null
+++ b/debian/rtsadmin.docs
@@ -0,0 +1,2 @@
+README
+COPYING
diff --git a/debian/rtsadmin.install b/debian/rtsadmin.install
new file mode 100644
index 0000000..1b129dd
--- /dev/null
+++ b/debian/rtsadmin.install
@@ -0,0 +1,2 @@
+lib/rtsadmin usr/share/python-support
+bin /usr
diff --git a/debian/rtsadmin.postinst b/debian/rtsadmin.postinst
new file mode 100755
index 0000000..184f745
--- /dev/null
+++ b/debian/rtsadmin.postinst
@@ -0,0 +1,17 @@
+#!/bin/sh
+for lib in lib lib64; do
+ for python in python2.6; do
+ if [ -e /usr/${lib}/${python} ]; then
+ if [ ! -e /usr/${lib}/${python}/rtsadmin ]; then
+ mkdir /usr/${lib}/${python}/rtsadmin
+ for source in /usr/share/python-support/rtsadmin/rtsadmin/*.py; do
+ ln -sf ${source} /usr/${lib}/${python}/rtsadmin/
+ done
+ python_path=$(which ${python} 2>/dev/null)
+ if [ ! -z $python_path ]; then
+ ${python} -c "import compileall; compileall.compile_dir('/usr/${lib}/${python}/rtsadmin', force=1)"
+ fi
+ fi
+ fi
+ done
+done
diff --git a/debian/rtsadmin.preinst b/debian/rtsadmin.preinst
new file mode 100755
index 0000000..c591c64
--- /dev/null
+++ b/debian/rtsadmin.preinst
@@ -0,0 +1,3 @@
+#!/bin/sh
+rm -f /usr/share/python-support/rtsadmin/rtsadmin/*.pyc
+rm -f /usr/share/python-support/rtsadmin/rtsadmin/*.pyo
diff --git a/debian/rtsadmin.prerm b/debian/rtsadmin.prerm
new file mode 100755
index 0000000..1dc3cb5
--- /dev/null
+++ b/debian/rtsadmin.prerm
@@ -0,0 +1,8 @@
+#!/bin/sh
+for lib in lib lib64; do
+ for python in python2.6; do
+ if [ -e /usr/${lib}/${python}/rtsadmin ]; then
+ rm -rf /usr/${lib}/${python}/rtsadmin
+ fi
+ done
+done
diff --git a/debian/rules b/debian/rules
new file mode 100755
index 0000000..81a22ef
--- /dev/null
+++ b/debian/rules
@@ -0,0 +1,56 @@
+#!/usr/bin/make -f
+
+build_dir = build
+install_dir = debian/tmp
+python = $(shell \
+ python=$$(which python2.6 2>/dev/null); \
+ if [ -z $$python ]; then python=$$(which python 2>/dev/null); fi; \
+ if [ -z $$python ]; then python="/usr/bin/python"; fi; \
+ echo $$python)
+setup = $(python) ./setup.py --quiet
+
+binary: binary-indep
+
+binary-arch:
+
+binary-indep: build install
+ dh_testdir
+ dh_testroot
+ dh_installchangelogs
+ dh_installdocs
+ dh_installman
+ dh_install --list-missing --sourcedir $(install_dir)
+ dh_fixperms
+ dh_compress -X.py
+ dh_installdeb
+ dh_gencontrol
+ dh_md5sums
+ dh_builddeb
+
+install: build
+ dh_testdir
+ dh_testroot
+ dh_installdirs
+
+build: build-stamp
+build-stamp:
+ dh_testdir
+ # Build the source package
+ $(setup) build --build-base $(build_dir) install --no-compile --install-purelib $(install_dir)/lib/rtsadmin --install-scripts $(install_dir)/bin
+ echo "2.6" > $(install_dir)/lib/rtsadmin/.version
+ # Cleanup
+ rm -f $(install_dir)/lib/rtsadmin/rtsadmin/*.pyc
+ touch build-stamp
+
+clean:
+ dh_testdir
+ dh_testroot
+ rm -f build-stamp
+ $(setup) clean
+ find . -name "*.pyc" | xargs rm -f
+ find . -name "*.pyo" | xargs rm -f
+ rm -rf $(build_dir) $(install_dir)
+ dh_clean
+
+.PHONY: binary binary-indep install build clean
+
diff --git a/rtsadmin-doc/html4css1.css b/rtsadmin-doc/html4css1.css
new file mode 100644
index 0000000..a1e39f4
--- /dev/null
+++ b/rtsadmin-doc/html4css1.css
@@ -0,0 +1,273 @@
+/*
+:Author: David Goodger (goodger@python.org)
+:Id: $Id: html4css1.css 5196 2007-06-03 20:25:28Z wiemann $
+:Copyright: This stylesheet has been placed in the public domain.
+
+Default cascading style sheet for the HTML output of Docutils.
+
+See http://docutils.sf.net/docs/howto/html-stylesheets.html for how to
+customize this style sheet.
+*/
+
+/* used to remove borders from tables and images */
+.borderless, table.borderless td, table.borderless th {
+ border: 0 }
+
+table.borderless td, table.borderless th {
+ /* Override padding for "table.docutils td" with "! important".
+ The right padding separates the table cells. */
+ padding: 0 0.5em 0 0 ! important }
+
+.first {
+ /* Override more specific margin styles with "! important". */
+ margin-top: 0 ! important }
+
+.last, .with-subtitle {
+ margin-bottom: 0 ! important }
+
+.hidden {
+ display: none }
+
+a.toc-backref {
+ text-decoration: none ;
+ color: black }
+
+blockquote.epigraph {
+ margin: 2em 5em ; }
+
+dl.docutils dd {
+ margin-bottom: 0.5em }
+
+/* Uncomment (and remove this text!) to get bold-faced definition list terms
+dl.docutils dt {
+ font-weight: bold }
+*/
+
+div.abstract {
+ margin: 2em 5em }
+
+div.abstract p.topic-title {
+ font-weight: bold ;
+ text-align: center }
+
+div.admonition, div.attention, div.caution, div.danger, div.error,
+div.hint, div.important, div.note, div.tip, div.warning {
+ margin: 2em ;
+ border: medium outset ;
+ padding: 1em }
+
+div.admonition p.admonition-title, div.hint p.admonition-title,
+div.important p.admonition-title, div.note p.admonition-title,
+div.tip p.admonition-title {
+ font-weight: bold ;
+ font-family: sans-serif }
+
+div.attention p.admonition-title, div.caution p.admonition-title,
+div.danger p.admonition-title, div.error p.admonition-title,
+div.warning p.admonition-title {
+ color: red ;
+ font-weight: bold ;
+ font-family: sans-serif }
+
+/* Uncomment (and remove this text!) to get reduced vertical space in
+ compound paragraphs.
+div.compound .compound-first, div.compound .compound-middle {
+ margin-bottom: 0.5em }
+
+div.compound .compound-last, div.compound .compound-middle {
+ margin-top: 0.5em }
+*/
+
+div.dedication {
+ margin: 2em 5em ;
+ text-align: center ;
+ font-style: italic }
+
+div.dedication p.topic-title {
+ font-weight: bold ;
+ font-style: normal }
+
+div.figure {
+ margin-left: 2em ;
+ margin-right: 2em }
+
+div.footer, div.header {
+ clear: both;
+ font-size: smaller }
+
+div.line-block {
+ display: block ;
+ margin-top: 1em ;
+ margin-bottom: 1em }
+
+div.line-block div.line-block {
+ margin-top: 0 ;
+ margin-bottom: 0 ;
+ margin-left: 1.5em }
+
+div.sidebar {
+ margin: 0 0 0.5em 1em ;
+ border: medium outset ;
+ padding: 1em ;
+ background-color: #ffffee ;
+ width: 40% ;
+ float: right ;
+ clear: right }
+
+div.sidebar p.rubric {
+ font-family: sans-serif ;
+ font-size: medium }
+
+div.system-messages {
+ margin: 5em }
+
+div.system-messages h1 {
+ color: red }
+
+div.system-message {
+ border: medium outset ;
+ padding: 1em }
+
+div.system-message p.system-message-title {
+ color: red ;
+ font-weight: bold }
+
+div.topic {
+ margin: 2em }
+
+h1.section-subtitle, h2.section-subtitle, h3.section-subtitle,
+h4.section-subtitle, h5.section-subtitle, h6.section-subtitle {
+ margin-top: 0.4em }
+
+h1.title {
+ text-align: center }
+
+h2.subtitle {
+ text-align: center }
+
+hr.docutils {
+ width: 75% }
+
+img.align-left {
+ clear: left }
+
+img.align-right {
+ clear: right }
+
+ol.simple, ul.simple {
+ margin-bottom: 1em }
+
+ol.arabic {
+ list-style: decimal }
+
+ol.loweralpha {
+ list-style: lower-alpha }
+
+ol.upperalpha {
+ list-style: upper-alpha }
+
+ol.lowerroman {
+ list-style: lower-roman }
+
+ol.upperroman {
+ list-style: upper-roman }
+
+p.attribution {
+ text-align: right ;
+ margin-left: 50% }
+
+p.caption {
+ font-style: italic }
+
+p.credits {
+ font-style: italic ;
+ font-size: smaller }
+
+p.label {
+ white-space: nowrap }
+
+p.rubric {
+ font-weight: bold ;
+ font-size: larger ;
+ color: maroon ;
+ text-align: center }
+
+p.sidebar-title {
+ font-family: sans-serif ;
+ font-weight: bold ;
+ font-size: larger }
+
+p.sidebar-subtitle {
+ font-family: sans-serif ;
+ font-weight: bold }
+
+p.topic-title {
+ font-weight: bold }
+
+pre.address {
+ margin-bottom: 0 ;
+ margin-top: 0 ;
+ font-family: serif ;
+ font-size: 100% }
+
+pre.literal-block, pre.doctest-block {
+ margin-left: 2em ;
+ margin-right: 2em }
+
+span.classifier {
+ font-family: sans-serif ;
+ font-style: oblique }
+
+span.classifier-delimiter {
+ font-family: sans-serif ;
+ font-weight: bold }
+
+span.interpreted {
+ font-family: sans-serif }
+
+span.option {
+ white-space: nowrap }
+
+span.pre {
+ white-space: pre }
+
+span.problematic {
+ color: red }
+
+span.section-subtitle {
+ /* font-size relative to parent (h1..h6 element) */
+ font-size: 80% }
+
+table.citation {
+ border-left: solid 1px gray;
+ margin-left: 1px }
+
+table.docinfo {
+ margin: 2em 4em }
+
+table.docutils {
+ margin-top: 0.5em ;
+ margin-bottom: 0.5em }
+
+table.footnote {
+ border-left: solid 1px black;
+ margin-left: 1px }
+
+table.docutils td, table.docutils th,
+table.docinfo td, table.docinfo th {
+ padding-left: 0.5em ;
+ padding-right: 0.5em ;
+ vertical-align: top }
+
+table.docutils th.field-name, table.docinfo th.docinfo-name {
+ font-weight: bold ;
+ text-align: left ;
+ white-space: nowrap ;
+ padding-left: 0 }
+
+h1 tt.docutils, h2 tt.docutils, h3 tt.docutils,
+h4 tt.docutils, h5 tt.docutils, h6 tt.docutils {
+ font-size: 100% }
+
+ul.auto-toc {
+ list-style-type: none }
diff --git a/rtsadmin-doc/rtslogo.eps b/rtsadmin-doc/rtslogo.eps
new file mode 100644
index 0000000..704c2c8
--- /dev/null
+++ b/rtsadmin-doc/rtslogo.eps
@@ -0,0 +1,281 @@
+%!PS-Adobe-3.0 EPSF-3.0
+%%Creator: GIMP PostScript file plugin V 1.17 by Peter Kirchgessner
+%%Title: rtslogo.eps
+%%CreationDate: Sat Nov 28 19:30:24 2009
+%%DocumentData: Clean7Bit
+%%LanguageLevel: 2
+%%Pages: 1
+%%BoundingBox: 14 14 156 45
+%%EndComments
+%%BeginProlog
+% Use own dictionary to avoid conflicts
+10 dict begin
+%%EndProlog
+%%Page: 1 1
+% Translate for offset
+14.173228346456694 14.173228346456694 translate
+% Translate to begin of first scanline
+0 29.996250468691414 translate
+141.73228346456693 -29.996250468691414 scale
+% Image geometry
+189 40 8
+% Transformation matrix
+[ 189 0 0 40 0 0 ]
+% Strings to hold RGB-samples per scanline
+/rstr 189 string def
+/gstr 189 string def
+/bstr 189 string def
+{currentfile /ASCII85Decode filter /RunLengthDecode filter rstr readstring pop}
+{currentfile /ASCII85Decode filter /RunLengthDecode filter gstr readstring pop}
+{currentfile /ASCII85Decode filter /RunLengthDecode filter bstr readstring pop}
+true 3
+%%BeginData: 12712 ASCII Bytes
+colorimage
+JcE[gJ,~>
+JcE[gJ,~>
+JcE[gJ,~>
+JcGQG!:J,3J,~>
+JcGQG!;4V:J,~>
+JcGQG!;at?J,~>
+JcGTH!q0"%cMrC~>
+JcGTH!qr);cMrC~>
+JcGTH!rL:1cMrC~>
+JcGTH"-jB]l/DXHrohbE[(c&'s*t~>
+JcGTH"2fJIo&0NOp?h#%k54lPJ,~>
+JcGTH"6%8Apu)/TrV?3\p@\H9~>
+JcGWI"mn#T%q>=prsS"_AO%Uj2K"k"A$u.4J,~>
+JcGWI"n[UqH)1>ursSMR_mHJOZc1%eUt#5?J,~>
+JcGWI"o?3S_peSFrsSbqlK.!lk3D-[e*?c/J,~>
+JcGWI!2TQ(!I+G(rt,1581,f6.T1;(;,V"[]$UERs*t~>
+JcGWI#0_:6G'L:jkPl+mi4-)&\?**IXIPoa^YeAVr;V9~>
+JcGWI#3EV4]trD$kPl(lo]PT5oAS]df?_gqiUQX=s*t~>
+JcGZJ!W#^l%fpeGl2MCED'gYY4*.(,X/;biR"B!YQg4PLJ,~>
+JcGZJ#lL0:F`hnPYPRM`'(*]B\?iN^e(E$sf\4m!^UqqfrVqB~>
+JcGZJ#l`/d]Y(tpf_k"6"o.EHoBkZ=lNueAmI'B*j5K>$rVqB~>
+JcGZJ!S&=i%foklm/I=JK.\Q#9pjl;rsAZ!q"=.@gr.kErr7K~>
+JcGZJ!pG)WrcJ<dH(aQkrs/,@YIUaFfCSq?s83&tq=X7>mf!09~>
+JcGZJ!qX4Ark&?I_U8)Drs/GhjRqNonG<(_s8E3#r;69\p\k,B~>
+jSo8ZrnIGRrVj_7!2TH%!Y@/fn,EXLL,LAIClm<;s*t~>
+jSo8ZrnIGRrVj_7!R#ZaFTZf5MU(\j#P-3HYIMHllL+\&~>
+jSo8ZrnIGRrVj_7!p751rO`6G`Q%-1rs/GhjlYaikk4BDJ,~>
+q5se'_s71""QQYf\bQ+&#N2nQMNFUVq#13qrPbn?h#@<Vi3'/lqYgEsrPbn@gACmTjg:;1OJ^D&
+rr3&tYF,-R#F;jus6H;Fh#@<Sb*W*q".H:;q"t'li0sJ0!LhpBrr@*8%g3FS=2"J'rsAGELJq9%
+LkDD`mJh\~>
+q6L.1a6W['"Qctr_"dj-#NN:aP*`2oq>L<rrQ):LhZ!NXij?#*qYgEsrlMIPh>@3Wkdd7IQ`S[6
+rr3&u[\<Mk#GAd1s6ZbZhu<WVc_193"/E3Pq>:0mj.lFE!MeZNrrK6IqK2pdG+[Icp&>?rm*`Ya
+b/V'6n*U.*~>
+q7$L:b3]'+"R!;(_YO-0#N`RlR%:D-q>L<rrQMaZi;W`ZjLDY8qYgEsrlqp]hu!EYke3^XS[$`D
+rr3&u];YP)#H#?;s6d%giVriXd\m&C!kHqZqu6ZXpSe/RqYpTK_tq"K_nFIbrUp0srqZ$Il0RQh
+j7_m?J,~>
+rbq^R")i(%]D_a4bAVEHnc&Ojrjm2irGW*[N8":2s8.gtF1_GO"N54uDS?#K"T+7$Eju#I"9#iU
+DuFVkCS(.ts7fqq9MA)N8P;`N:2g.3fgSNFrr3%g91f+V"%l%#`;KW;`(*Blqb.#`^\[s2>7gQ?
+"V;8=Ph#O(rs@MbOH>NH:h@66m/MS~>
+pj*$eK#.HcrrgVnI=h%hrt>=FI<p6dH[Bt$h>[HSqilMX_#F?:aEr8Y_YsK;r0MhZ^A\'RrP*l/
+H[:!bFen[0s7pJ<?!U]<=^56<@!hnQgf7Oorr3;!>?Y<=?!C?Vao)/@a]M_Pqd';/`qo]9TmZZY
+#'YE;R)8%`q>UcniR?/nYaD6;qs=?5~>
+rIY!#"G2/\`;]c9"OVgTM"CWG"TH8lJqX,U$@eN$rr<#qRtc[8rr3,EJqJTmrVm#uSVDg6rVluu
+_hWjBK,8s9o`+seGALu-BOtagBk_C:rr:kaB#XcL$)hKKC2.Hq@>)o'rr^MVASM'W!c)MlqYpWE
+aM%G_#/8'hc/J9mq>Ud!p$hA1g!%n1rTsQ7~>
+rb;FLrc%mWs(qdQ_Z'Q<c"_'Do)AY%aD5X0Df'E2CiOLTs8VtZD/I&errpGXC2J.Trr3,uPAP-K
+rVmtHCi+$.DK'K0E1Qn+qMT-*D.#e$A8Q-uQN.!=9h)nsrsGAg:NCSq=@c(UX8W&$a$rO7G5ZOb
+FEZ11rrUS9&GH\K&.]UABSi@bZa6d#G@3lc)D@3GlMlA~>
+s)\9aGQ)shHinS;G/jO]"OM77L[tHE'$GEAG^4LYH?OX^jT#8WR!3emrr3/FF`_hXk5PA`r0qnR
+_#=9T\p&CDH?aa\G'g%_s8/(+H$X=-<H3>1H(Y*9h-="!rr3>'=C-H-H=g8`=gS),"O'nIH%Q!G
+!e>mTqYpZYPC%NLFVK%GH$YRO]>r=Je'Gt>V3HITMSo;Us*t~>
+s*OiqJ,G!"J,t,@rr3,MI"..Irr3VVIXcm"It<3#KS/Fes8/C;JB%Tg"jVLCIu!4drri7nJ:>H8
+ru\t)J:N9'Jq/<&WVlenSUlBFG@4g)K7\]Ps8V"i@EAHJ$ER`NLP^Y&A7oD^rVm#H@:=bFr.tH3
+gA:gOo#o%fq7IEV`5KC3f&#NPl07BkgXF<\^;f43lMlA~>
+.V`a8JZuDaYF]iOD1;0Xs35:pJ+EX>qM]<-I_!_;ODJg3p&G'hPAP0Orr2u=rb;aWQ21I[qikr6
+]`.pRgM^nEGH&KcI;Nk@bl@_>q"Xa'77kl&q"X^cs4U4;^]+6@T1E_is8(ud>"M?1rr3)@78D_0
+rr_P&&J4XHrXer;rt#8K5\[pqs*t~>
+.WffVMRL"'[%r.nG_c"ms3Pk3M":TGqNQ5IL;;0QR!*_UpAb0iR<Nnnrr3GNF`__MSGN9cr1%tS
+_#F?VhKa<hJ?R#&L3Ilhci=%Aq>'s/<E[iKq>'pfs4ps]`;]cEVce6:s8)&lBNeTdrr3)E<F+PQ
+rrhnnGC"H.FV&_?H@(!aGBS.RH$t'aPI9K8s*t~>
+.XZYnOh/0:\Z1@4J<'@'s3l@FNq<;NqjN(aNPa/aSUQ*opAb0iT7;C2rr2uDrd5$&U&+fhr1\^j
+`;]cZi.-<,LU>49NIZS5df9@Eq>'s3?t@peq>'sgs57R!aSu2IXCZqVs8),sEb/e1rr3)H?teQi
+rri/K^;[h^]F_cl`5]d5^V7Fs_o^'7bMMpUs*t~>
+");XhTD\^%a_ts2h>dN"C2ACjs8VG1Chq/lrr`/-Y5J;!"T+C"D86)M!6>(R&Sl3ps8Vt\D/?lb
+s8AL*Cj=\5rrhA/G+[[hrr^A38_<k>"56L>^]+68T1EblrVm5P<(V<2s8U,.:>bO?"0b*&&FpA3
+&-WS,D0(<LkPp&~>
+"*A^1Vu6Q-b^">Ri;`i(FE`Q3s8VM<G'2%.rr`/4ZMa_%"T+U2Gf0O\(!C`AFEMcCs8W"eGBUe%
+s8Aa<G(SQLrrhG:J#;6&rr^MG=kroS"5R6``;]c=Vce9>rVm5V@p4K^s8U;C?/k>Q"k&TZI!Bi/
+FUE;9G^Fj]NNBh=m-"D!~>
+"+5QIX8Mu1c[gLmiW&r,I".1Js8VPDIXTQArr`/7\,?7*"T+g@JB%Tg!7(Rk&Uf,Hs8W"kJ:>H:
+s8ApIIYlt]rrhJBL9'A3rr^YVADd:a"5mj$aSu2AXCZtYrVm5XD.SV&s8UDQB]SX^"mO4Ia2#C`
+]F2Ee^VRn2]u\gcmeQ&GJ,~>
+");XhSc&L$n98\_dJs6kC2ACjs8VM6Ci#m6q#:KoPAP0Orr2u=rb<![Cj"e<qikr6]`8#uG&2!(
+r;QfSjSAfXa$iSLqu6cP:.E"trr\laCA%N)$(FIZhuE_s78Db.rr_eg;]G]Iq[`W:#SfF@Q]5F$
+_#3BqJ,~>
+"*A^1V>U?,nUPe(eGoQqFE`Q3s8VSAG'9kQq#:KoR<Nnnrr3\UF`__KG(8WRr1%tS_#OH%J9H+J
+r;QfUk5##Zb>h]uqu6cS>unoCrr]0%GP1n6$)C[.iW&r%<F+SOrs8>BSU>a2H?jc7G7AeBH@1$`
+N2stD_p7ECkPp&~>
+"+5QIWVlc0nq_aBf)Pd!I".1Js8VVIIX\Hhq#:KoT7;C2rr2uDrd=`p%Yj0Hr1\^j`;fl*LOO`f
+r;QfWkkY5\cX:;=qu6cVBOT!]rr]?6JG9!A$*%KIj8]/*?teTgru(^rdD3]@`59=)^VI\&`Q,s2
+`ms/:lL+?EkPp&~>
+");XhTD\^*d;Nf9h#IE!C2ACjs8VtYDea*:T>'.Zr;QosPAP0Orr3\QC28?LD/PR7rKD)7]`8#\
+CM\(Qo)Ag477l#+rr^tM7_es&".r,!p&+grcq6dAs8U,.:?(X?(B2OCA3_U`'GM<*0M-5R]tBV-
+4.+"<k5Tr~>
+"*A^1Vu6Q2e9Q1[hZ*W&FE`Q3s8W"bH$"7aV8D-fr;QosR<Nnnrr3\UF`rRrGBfSRrL8"S_#OGd
+F`r/oo)Ag8<E\#Prr_(_<lG";"/f:Gp&+gre65qks8U;C?/t8N(#m--ObA+,G^4dpQ)hgViRkoP
+f&lA^s*t~>
+"+5QIWr2l5eq&9ui;`i+I".1Js8W"hJp`',WlF#qr;QosT7;C2rr3\XI"%-:It=9jrh=pl`;fkj
+I=?k4o)Ag<?tA'irr_1m@EAHJ"0?'ap\b$tf48@2s8UDQB]\R[(%UA5b/;$.]tV;$bLY\?o()//
+nauS<s*t~>
+"DV^gL;!T#%'P"nD1)!Us35:pJ+EU=$IX!7Ci431DiMa5rr3,uPAP0Orr4"ZC2K9YD/=%&s-IS=
+]`8#LCi"Xns8W)knac8@p&"aka$iSLqu6cP:.E"trr\laCA%N)%HUY]WrN+>7S%`TJV(#us8W)n
+rt>"cf?r(#jR2EeWG4+8@aDWdi9(&i~>
+'QeJ@O2AcO_R+u>GD5_is3Pk3M":QF$IsNMG'J@ZH'QJKrr3,uR<Nnnrr4"^Fa0G$GBJ)Hs.=OZ
+_#OGVG'8]6s8W)lo(2JDpA=jlb>h]uqu6cS>unoCrr]0%GP1n6%HqD+ZiC'L<`js5N/b4=s8W)n
+rt>.skiLsimI^,AiQ&12_sH@9o&ft&~>
+'RY=XQ,^ha`jgtVJ;j1$s3l@FNq<8M$J0r_It<6'J==^[rr3,uT7;C2rr3h\I"7s<Isldcs.k0o
+`;fk\IX[COs8W)lrUU$_qu6cE?tA'irr_1m@EAHJ"0?'ap\b%#l#1(@s8UDR@Z$D7Q+?Yh!<2`m
+')hRso()DGq>:$^mH!d%q!meJk5Tr~>
+rb;FLrGMgXD/F+&rr3,GC2ACjrVmJYN+m3KDJa?0VYgDnqibl6^]+6Sa(fISrLn+FG3,E7CqTfJ
+]khM;p&G'fR=Ba;J"-62rr^;18_!Y;"56L>^]+68T1E_ir;R5k>t:gms2RSt9MJ5WGk_4Argo1f
+kPFuR%e\M]L0$)W.6=EnSX["[k5Tr~>
+rcA<eH$Xa]HiA3h_Z'Q<cuaJfo)8S#es(9&H?aX[Gc^fBs8/4.Gf0O\*m8\NRJj%/G(AeTG]pn&
+s20#(N;!;OqOi\$Ll%l%r;QlB<E\#Prr_(_<lG";"/f:Gp&+h!m:ob+s8U;D<aB'>@Y=CS"PM;f
+o_nRb%fPtYdCufl]X-Dee^`RSk5Tr~>
+rd4]prIP$&rdOperr3,MI".1JrVmJ^RXB^=J:E3%Z2=S$qk&@g`r>uTc?t/7riC["LZtmjI`29d
+`I<<#p](9iV>ZGUVtp8j"4(@ch#%*Ri+QQ?rr3)%AUsOQrseC<A@2C:cXC6rCMRjHpAY9dnFleZ
+p\t^#p$_,)nG22;qs<\>rT=-1~>
+rb;FLrbr$VC2/(*rVlg"c"_'Eo)/M!qUi3@H#7A6D9DnYqibl6^]+6Sa(fIRs6b;ODO-T)CqTfJ
+`G9:<nc/X`J9#RGE/"#`rr^;18_!Y;"56L>^]+68T1E_ir;R5c<(F(js2RQ"JWGeJ[J'V>N$p&7
+:hkB7OH>6FC/I)E-7:/h.XgF<HBk>Mjo9i~>
+rcAHiH[C']FEE/LrVlg"cuaJfo)/M!qV/QNJp2B]GL$6gqjDYQ_uBZWbB8&ss6k\dGb1IMG/4.X
+b&ql^o)JabMLKkrH]@dtrr^GE=kW]P"5R6``;]c=Vce6:r;R5e@p$,<s3+JFN0ors]Cu7Dd^Q6^
+]>Vq@e'l[f`OiIYY-YLDXN^A-`R3B'jo9i~>
+rd4]qrdtB'I!gggrVlg"dsQY*oDJUlqVAl[M1>MR#LNMNqk&@g`r>uZc?t/6s6u"tJ>8lhI`29d
+c$b&#oDejcOGAF<KU)<0rr^STADI(^"5mj$aSu2AXCZqVr;R5gD.C-Vs3G(_PFnD8_"RdInFGT"
+kj%O'nF5l5lf[?gkO%^-ipHX<jm2@-jo9i~>
+$>O?lI%R."Chddkr;QoEC2ACjq#:ch]6.n,T`>&hPAP0Orr4%[C2K+3s2o+mC2n!)]`8#aCMRkD
+s8VnSFE)&3MYI&K"33i%eGK7Jg.k,Prr3(m88@hqrs?b;7`GE-a$ieao)Ag--mg;8-iPG"-RE3F
+s1Vm$Z\eJ=s*t~>
+$?UE5L8Lo?G'%r;r;QoHFE`Q3q#:ci_1$NOVuQeoR<Nnnrr4%_Fa0;Qs3>b1FaJ4R_#OGgF`r&e
+s8Vq\IsQ3\P5"nS"3X\Ig&(dOh-F("rr3(u=a0Q:rs?nO<m(IBbZ/!2o)BZlXKJb3['d6GZF%$H
+Z*1:4ZE1q/s5Uc<hou#[s*t~>
+$@I8MN2s(UIXHXVr;QoKI".1Jq#:ck`.i\jXT/=tT7;C2rr4%bI"7gfs3Z7DI=clr`;fknI=?b*
+s8VqbL4Xf&R/$UZ"4(@ch#%*Ri+QQ?rr3)%AUsOQrs@%^@F"oQcX:JLo)B[&io]FimdKQ0m-X90
+lfdHmm,[F,s7>77n`]u6s*t~>
+$YjKpScA`eODSfdoDS[lc"_'Eo)8RhiVNKP$DhTVQ2L[ZPAP0Orr3\QC2K+3s8Sd1CMR[$]`8$"
+H>IGqrr32mo;ukHQ2LR["33i%eGK7Jg.k,Prr3(m88@hqrs=HO;sF>^a$ib\o)AdX7OIgT%43GK
+>eC%'OBn!dB>4!!s*t~>
+$ZpQ9V>pSmR!3_3o_ndmcuaJfo)8Rhj8/]R$EJApSG`EaR<Nnnrr3\UFa0;Ps8T!AF`heL_#OH'
+K6DI?rr<#i"LE;sSG`<b"3X\Ig&(dOh-F("rr3(u=a0Q:rs=fj@dF'obZ.s-o)Add\$#Y/s/Z*u%
+BZt;^uYOid)48n^?#1Qs*t~>
+$[dDQWW3"qSUZ*Op&4mndsQY*oDS[ijneoT$F#&1UAY&gT7;C2rr3\XI"7ges8T0NI=6Kl`;fl,
+Mgg2]rr32ooXB=#UAb#i"4(@ch#%*Ri+QQ?rr3)%AUsOQrs>''D!_3%cX:GHo)AdhjlQI"rSmh8%
+-?m9l1Y/Sm-X-1iUl42J,~>
+");XhSc8WlcYRB6^A\'>c"_'Eo)Jag_0)C4rr3ArNbiR)s8Vt[D/I&eruK-pD3=ess6kDRC2.Rt
+s8W(nDJO1+qu?][KktY3qu6c=77l#+rr^tM7_es&".r,"r;HX&[7a&Lrr<#=78N"7rrINppa$Ft
+-S/c>s8T]E?^/iJLY:nQs*t~>
+"*A^1V>gJtdWTbW`;T]DcuaJfo)Jah`IF`Jrr3AsQ?IMMs8VtaGBh%)ruK:*GaSI0s6tbeFED]A
+s8W(tG]e;Nr;Zf^NcoZUqu6cA<E\#Prr_(_<lG";"/f:Hr;HWt]N\R+rr3)E<F4eWrrLBKq5P"-
+Yd:IKjo>A?^W4df^!#s?jo9i~>
+"+5QIWW)o#e:)gqa8Q#GdsQY*oDejiaG-_Zrr3AtS:?'is8VtgIt5Q<ruKC4J=c`>s6u"tI!gC]
+s8W)%JUN*jr;Zf_P^\1qqu6cE?tA'irr_1m@EAHJ"0?'brVc`u^h%)Krr3)H?tnforrMNNq;DnF
+l0[KppAb0`j7DU0iTU4=jo9i~>
+$u0TqSGrQgqMoE0K(]-C#L.""J+EX>aSp&fFdAu$Jno.>_uKc6PAP0Orr3,AC2K+3rr3/GChmd%
+]`.p>j)]'QC6,9`GAM,;jSSrZa$iSLqu6cP:.E"trsPGiA_Z;'aGNM[>iYh`"NNr*a6!3Y!;?6g
+!R*%k-3#;(-RM32c2[gkJ6H6FdVrL.dcBNiJ,~>
+%!6Z:V#LDoqNc>LMt[/M#LIR:M":TGbl2htI\*[@MKO&da8c2:R<Nnnrr3,EFa0;Prr3/KG'.nM
+_#F?BjEu2qFI0,*J9H-ak55/\b>h]uqu6cS>unoCrsP`-F5H'=b`l46C?GNq"O'kNbielc!;H<h
+!pm%jri-[5XK]+8[a+4=s6@_@`7+%i]&rMTjo9i~>
+%"*MRW;chsqOE(cP4nnT)Uj(`Nq<;Nc[^RtL82&WOaVY.b5_M=T7;C2rr3,HI"7gerr3/MIXQTm
+`;]cFk(J86I%@RBLjsr.kPP8]cX:;=qu6cVBOT!]rsPo=I,O5Ld%"TYFQi`("OCIgcfk8h!;QBi
+!r1g9ro"RNjQZ-qk3_pCs7PO<md0Z/i:d-Jjo9i~>
+"D_jjSGrNf"O(_!D8?/N)U)u6Ie*O=jG7bmDJsK5DJa?Oo`+sgP&>0Orr3,@CMo74rVm"nDJO+$
+rVm5CDf9H1E,]]5DSGuI"3*f$eGK7Jg.b)Orr3=s84QQV:I[uL@aP)R"NEl!:fI9i!E@5;ru1fG
+-RL/f-RM3%Z0VGig6#TF>G9Pd>Fs!ak2lVn~>
+"Eep3V#LAn"OM@9GK'R])UEPNL[tKFk)an6G^=^_H$=V"p&G'hR!<nnrr3,DG'TGQrVm"uG]e5F
+rVm5HH$OUZH[9p^Gf0CX"3OYHg&(dOh-=%!rr3>&=B8j??<:B9E7Fjd"NseG?Xs"C!FsIOru1mR
+ZECI>Z`COViV*<On'JV_^";Vf_:Iu@p>u=)~>
+"FYcKW;cer"O_dKJ&qWh)Ua%aNV!2MkEggOJ:`E+JU`?ApAb0iSq)C3rr3,GI=[sfrVm#%JUW*d
+rVm5JJV&B'K7e`+J&_?b"3t=bh#%*Ri+HN>rr3>+@ps.hBjtagH.W#p"O:C`Bl*``!c2`%q>VB4
+l0INtmd9#rm.C5Us7ts<mH<m1jQu:4r;GaWJ,~>
+"E&0pRK!3c%/Gg#E..<Ws3#;!HL_"7"7N::DZ4YYE,qT;rr3,uOE#9Qrr3,?DK1[7rVm#gJ8fJ<
+r;QiCLMZs1!c<S0q>UQ97n(o(rr^nE8A5$&$(FLW8P)HCA=D`[rrU//8,>dX8AY*$#KS_Y959[+
+m/I"pd#hU=>?\>]Dd@>(`6[Z/s*t~>
+"F,69U&P&k%/Q07H\V.ls3>k9KCSs@$Lt?QG^XsfH@)LVrr3,uQ@""qrr3,DH$kkTrVm#hM0aK_
+r;R&LO*5o!I<UUmq>UQ==B=)Orr_%Y=i11<$)Ca,>$>'1EMMgsrrU>E=Sc2%=igC<#O8Ot\]*+^
+q#::'lHm,Y^q&=g_Sa4UlL+E.s*t~>
+"Fu)QV>gJo%/QBEK8oO's3Z@LMXpcH"7`dVJc:B'K)r"@rr3,uS:cO6rr3,GJ;'HjrVm#hO+W&$
+r;QiIQ%/de!e6BYq>UQA@q"-hrr_.gAB+WK$)qKHAnGR_HE$6.rrUGSA,Bj>B4]9]rs/;Zj6#[t
+p\b$j%e]/Glg!HupZ(]$q>C0ijo9i~>
+"3JX,h#.0[ilB0^nc/XVaiMm,r;Qucag8+S_r:On"T?uoa63I%"7+(MfDGRMilK4'qu6lmcEsa]
+c11T/"7*V5n,*+eo"_m#rr3#T])K>D_pn2PrrD?Z]E,_3p\t<Gh":RF&a$KP=Bep4B#Dc*>?\JM
+d)GD6s*t~>
+"3o'8hYdB]jibolo)JaXc-4]7r;Quec+(*h`oI!s"T@&ubN]$+"7=@WgACmPjNPm4qu6lmdCHTo
+d.6u3"7<tCnb`=go>SQ2rr3#W_#D1Pak61\rrDE[_>rutrrVZZqu-O,o\[d"_7RIsjljOq]&Dl@
+lhTZBJ,~>
+"45BAi;ET_k0D?"oDejZd*L;?r;Qufcb-a!b2iL#"T@0&cKbE/"7=I]h#%*Rk0ME=qu6lne@iB*
+e+<A7"7X=No)&FhoZ>)<rr3#Y`W!jYbhMacrrDK]`W5H$s8Voqrr)j+qXE\)lf@<koC_20iUd'H
+!<2*[J,~>
+JcGKE(B*[MYD?ao>Zb*4He#`8>$?s3ZeW.ajo9i~>
+JcGKE(B4'ph9<RJ^q.+ma6qs"]tM_Yj7;@0jo9i~>
+JcGKE(&n+)n*&crlJgggk4eH+k3)"+qu$?Ps*t~>
+JcGNF([Zrh>$G67='&R,S`$/C=]oRe\\cn,pZ;F*~>
+JcGNF(\c+f\A5nm\@9/]ea_8%]=YlFkihNtr8ms/~>
+JcGNF(]3FWhs'Igi8<_UmJ6/.jQ,@srVQQiroO01~>
+Y5]rpT;0s4_".O*db<uaT=jpGW2Ri<fqb:fs8L3kY+OJ,W1_uHppZiqkk4rXrOZ3:r`9Y6>AqL4
+JQl5Y>J/"ef>PE$jo9i~>
+Y5]s8f%BBgjSJoVlgOQ$f&?Pmg=kcjnCQ7Ws8V<Mh:;#cg=Gfoqr$&[p%A=oro1N(\@8rY^r#7%
+ahRhc#35%;n`f9:jo9i~>
+Y5]sDmI'`JoDS^hpA"OQmIL5Mn*foKq!RPCs8VcdnEfrGn*TrNrpfCEqXsk(rpo%3i8<GPlK.[4
+kiq*ciUd!Ir;-<Os*t~>
+YQ$-*MVH::m?GNgr/8d]_V:I"kCKMjhM]h4Mq%caLX*teS'f!0qm2pQZD[ZnrtW@W>$,!7T&6nb
+>?=m0T%CEYij?`)jo9i~>
+YQ$-@c1'rDpX/.'rls]RjmU[^oZ>eXn]^@ecL(YUbNe<3eEc/?rSQ2sh:_f"rtXV-]t(qif(%Y2
+^UUeeea_qdo],69jo9i~>
+YQ$*Hl1jr:qs3nSs697AoD%YDqWI;Bq<7SIl1b5BkP"Q1m.U&8rpfCOnF#oJ'_:;Iip#Cfq!mD.
+i8EbarVucoqtoISJ,~>
+YQ$,qMY@)Lrkt6)hi@&Qh#IENs.%C7VSE02_4:IfeGoE(huE>-oq8Acs8)`drtEId=bkDOJQc2X
+='C=7[^XE6\'a::J,~>
+YQ$*9bkqG>roV]0n^$sNnc/Xfs4$?6g#N,4jOF1Um/R%0nc/I2r79U%s8;Hf'Bd*Abj4T;]XtY^
+^<?*/mHjB0mc=Cu~>
+YQ$*Fkl(P\rq,RWq<In?q>^Kos6K@XmdpJYoBcJAp](6Xq>^EXrU'%Qs8MTh'D1>Fkk=]4k2bFW
+ki_mAs82`or8dm.~>
+YQ$*GUkkYps6kg9[&j;KL6fUNs-_%0OHQ?3QD45Bi;`J7l2U12p#V6pXR#EV'&UaTP$2$k='&UL
+gV(#"i5<%YjSs`~>
+YQ$*LfZVk6s7a_5hqS1obL5JNs3p64cdC@rdb"EFnc/L7pAap5r:J=\g\:7<'(P/Ld(-p$\$s,k
+nEB,to]b`5jSs`~>
+YQ$*NmHEp2s82$YnaHMMkj.j?s6K@XlK[d(lg=];qZ$NZqZ$NYrVGgCn,)YT')D4nlJ^schr!YZ
+q>C9ir;6NljSs`~>
+XoBj5^Osu7UV2oFs8V`;P,A5Ib5[[Im-=*<r0?+<oonS=mZ$l=s4jW5n*p>g]Ob>O=BA^FeAeo'
+_;qo*k2cPm~>
+XoBjCj3$`gf](<Js8Vr\dFnQKl2T,op\+F^rmC99qpG*:ps/a9s7+jhq!e:pikE:D\[T;imHa'$
+kk=)sp>l7(~>
+XoBjIoB5iImdoc=s8W&klL+]<p&F=MqtpBmrp0C[rTa7ZrTX:\s7l'IrUBguo&K-$iSWk[p\b'k
+qYpEirT+!/~>
+YQ$-8iW&q[X8gRiO8AY$s8V56qNoV0WiZ7#l,ERbh#I&1kPst/o[rj:s+o:qrt"@nE`l^fdE/l%
+ZJ<d:Zh!2WJ,~>
+YQ$-Fo)JaGg]-TYci*e/s8V`5rR1$3gYDi/p?)A$nGiC4p&Fg3qX47\s3:$$rt"eV_n="+lg4$"
+kOS$/j8.j:J,~>
+YQ$-Lq>^K`n,N+ClMgeWs8VuZrp0=Yn*g;VqXa^Oq>^EYqZ$NXrV-9js604GrsnqpjlQ$np\k-i
+s8DipqW%U+~>
+YQ$--OL;WWg]-qfYlFBfUV)r!rh>Y*kD6.LNPPOYh>d/3kPst0p:c^%WL38DrseJ8U"@T#\%K?9
+ZF[dHjSs`~>
+YQ$-Cd,<j-nGiF(h>dE(fAb68rn#QaoZZ%JcK,#Qnc/L6p&Fg4qr?&^g=,lbrsebbfC/1tmITK7
+j7)71jSs`~>
+YQ$-JlLFH0q>^ESnGiIRmIKT4rpB"EqWRD=kk"i>q>^EYqu?WZrUT7En*Bo=rri8kmeHe\rrr<!
+q>^<jjSs`~>
+Y5]s7]=?i"s8A#8s8Vo8[ESXMm((UFmCTj[s8V!Ks8:aKs8(OKs4+TnoB?2[mH!$PkjR3`n)OFu~>
+Y5]sDiSjA-s8L@7s8W&\hs17ApYkN>pYt-!s8VWcs8DNds8DHds6\_)qWRqbq=X@Lp\OFPqW%U+~>
+Y5\V$o'uPTs!$b's8W&jnb)q^r:9U^qss:Os8Vons8Mins8D`ms7c$SrTO7err)fnrr2ipro=$/~>
+WrE5#i2`:DT`9N~>
+WW*(jgXQ4qs*t~>
+WW*%qn*H\HJ,~>
+WrE1r\#AVbs*t~>
+WrE2!i7[grs*t~>
+WrE2"na?:Hs*t~>
+JcE[gJ,~>
+JcE[gJ,~>
+JcE[gJ,~>
+JcE[gJ,~>
+JcE[gJ,~>
+JcE[gJ,~>
+%%EndData
+showpage
+%%Trailer
+end
+%%EOF
diff --git a/rtsadmin-doc/rtslogo.png b/rtsadmin-doc/rtslogo.png
new file mode 100644
index 0000000..dc5901b
--- /dev/null
+++ b/rtsadmin-doc/rtslogo.png
Binary files differ
diff --git a/rtsadmin-doc/voidspace.css b/rtsadmin-doc/voidspace.css
new file mode 100644
index 0000000..050d9f0
--- /dev/null
+++ b/rtsadmin-doc/voidspace.css
@@ -0,0 +1,137 @@
+/*
+:Authors: Ian Bicking, Michael Foord
+:Contact: fuzzyman@voidspace.org.uk
+:Date: 2005/08/26
+:Version: 0.1.0
+:Copyright: This stylesheet has been placed in the public domain.
+
+Stylesheet for Docutils.
+Based on ``blue_box.css`` by Ian Bicking
+and ``html4css1.css`` revision 1.46.
+*/
+
+@import url(html4css1.css);
+
+body {
+ font-family: Arial, sans-serif;
+}
+
+em, i {
+ /* Typically serif fonts have much nicer italics */
+ font-family: Times New Roman, Times, serif;
+}
+
+a.target {
+ color: blue;
+}
+
+a.target {
+ color: blue;
+}
+
+a.toc-backref {
+ text-decoration: none;
+ color: black;
+}
+
+a.toc-backref:hover {
+ background-color: inherit;
+}
+
+a:hover {
+ background-color: #cccccc;
+}
+
+div.attention, div.caution, div.danger, div.error, div.hint,
+div.important, div.note, div.tip, div.warning {
+ background-color: #cccccc;
+ padding: 3px;
+ width: 80%;
+}
+
+div.admonition p.admonition-title, div.hint p.admonition-title,
+div.important p.admonition-title, div.note p.admonition-title,
+div.tip p.admonition-title {
+ text-align: center;
+ background-color: #999999;
+ display: block;
+ margin: 0;
+}
+
+div.attention p.admonition-title, div.caution p.admonition-title,
+div.danger p.admonition-title, div.error p.admonition-title,
+div.warning p.admonition-title {
+ color: #cc0000;
+ font-family: sans-serif;
+ text-align: center;
+ background-color: #999999;
+ display: block;
+ margin: 0;
+}
+
+h1, h2, h3, h4, h5, h6 {
+ font-family: Helvetica, Arial, sans-serif;
+ border: thin solid black;
+ /* This makes the borders rounded on Mozilla, which pleases me */
+ -moz-border-radius: 8px;
+ padding: 4px;
+}
+
+h1 {
+ background-color: #444499;
+ color: #ffffff;
+ border: medium solid black;
+}
+
+h1 a.toc-backref, h2 a.toc-backref {
+ color: #ffffff;
+}
+
+h2 {
+ background-color: #666666;
+ color: #ffffff;
+ border: medium solid black;
+}
+
+h3, h4, h5, h6 {
+ background-color: #cccccc;
+ color: #000000;
+}
+
+h3 a.toc-backref, h4 a.toc-backref, h5 a.toc-backref,
+h6 a.toc-backref {
+ color: #000000;
+}
+
+h1.title {
+ text-align: center;
+ background-color: #444499;
+ color: #eeeeee;
+ border: thick solid black;
+ -moz-border-radius: 20px;
+}
+
+table.footnote {
+ padding-left: 0.5ex;
+}
+
+table.citation {
+ padding-left: 0.5ex
+}
+
+pre.literal-block, pre.doctest-block {
+ border: thin black solid;
+ padding: 5px;
+}
+
+.image img { border-style : solid;
+ border-width : 2px;
+}
+
+h1 tt, h2 tt, h3 tt, h4 tt, h5 tt, h6 tt {
+ font-size: 100%;
+}
+
+code, tt {
+ color: #000066;
+}
diff --git a/rtsadmin/__init__.py b/rtsadmin/__init__.py
new file mode 100644
index 0000000..90150a8
--- /dev/null
+++ b/rtsadmin/__init__.py
@@ -0,0 +1,24 @@
+'''
+This file is part of RTSAdmin Community Edition.
+Copyright (c) 2011 by RisingTide Systems LLC
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU Affero General Public License as
+published by the Free Software Foundation, version 3 (AGPLv3).
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU Affero General Public License for more details.
+
+You should have received a copy of the GNU Affero General Public License
+along with this program. If not, see <http://www.gnu.org/licenses/>.
+'''
+
+from ui_root import UIRoot
+
+__version__ = 'GIT_VERSION'
+__author__ = "Jerome Martin <jxm@risingtidesystems.com>"
+__url__ = "http://www.risingtidesystems.com"
+__description__ = "An administration shell for RTS storage targets."
+__license__ = __doc__
diff --git a/rtsadmin/ui_backstore.py b/rtsadmin/ui_backstore.py
new file mode 100644
index 0000000..d8595d8
--- /dev/null
+++ b/rtsadmin/ui_backstore.py
@@ -0,0 +1,482 @@
+'''
+Implements the rtsadmin backstores related UI.
+
+This file is part of RTSAdmin Community Edition.
+Copyright (c) 2011 by RisingTide Systems LLC
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU Affero General Public License as
+published by the Free Software Foundation, version 3 (AGPLv3).
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU Affero General Public License for more details.
+
+You should have received a copy of the GNU Affero General Public License
+along with this program. If not, see <http://www.gnu.org/licenses/>.
+'''
+
+from ui_node import UINode, UIAttributes, UIParameters
+from rtslib import RTSRoot
+from rtslib import FileIOBackstore, IBlockBackstore
+from rtslib import PSCSIBackstore, RDDRBackstore, RDMCPBackstore
+from rtslib import FileIOStorageObject, IBlockStorageObject
+from rtslib import PSCSIStorageObject, RDDRStorageObject, RDMCPStorageObject
+from rtslib.utils import get_block_type, is_disk_partition
+
+class UIBackstores(UINode):
+ '''
+ The backstores container UI.
+ '''
+ def __init__(self):
+ UINode.__init__(self)
+ self.name = 'backstores'
+ self.cfs_cwd = "%s/core" % self.cfs_cwd
+ self.refresh()
+
+ def refresh(self):
+ self._children = set([])
+ for backstore in RTSRoot().backstores:
+ backstore_plugin = backstore.plugin
+ if backstore_plugin == 'pscsi':
+ self.add_child(UIPSCSIBackstore(backstore))
+ elif backstore_plugin == 'rd_dr':
+ self.add_child(UIRDDRBackstore(backstore))
+ elif backstore_plugin == 'rd_mcp':
+ self.add_child(UIRDMCPBackstore(backstore))
+ elif backstore_plugin == 'fileio':
+ self.add_child(UIFileIOBackstore(backstore))
+ elif backstore_plugin == 'iblock':
+ self.add_child(UIIBlockBackstore(backstore))
+
+ def summary(self):
+ no_backstores = len(self._children)
+ if no_backstores > 1:
+ msg = "%d Backstores" % no_backstores
+ else:
+ msg = "%d Backstore" % no_backstores
+ return (msg, None)
+
+ def ui_command_create(self, backstore_plugin):
+ '''
+ Creates a new backstore, using the chosen I{backstore_plugin}. More
+ than one backstores using the same I{backstore_plugin} can co-exist.
+ They will be identified by incremental index numbers, starting from 0.
+
+ AVAILABLE BACKSTORE PLUGINS
+ ===========================
+
+ B{iblock}
+ ---------
+ This I{backstore_plugin} provides I{SPC-4}, along with I{ALUA} and
+ I{Persistent Reservations} emulation on top of Linux BLOCK devices:
+ B{any block device} that appears in /sys/block.
+
+ B{pscsi}
+ --------
+ Provides pass-through for Linux physical SCSI devices. It can be used
+ with any storage object that does B{direct pass-through} of SCSI
+ commands without SCSI emulation. This assumes an underlying SCSI
+ device that appears with lsscsi in /proc/scsi/scsi, such as a SAS hard
+ drive, such as any SCSI device. The Linux kernel code for device SCSI
+ drivers resides in linux/drivers/scsi. SCSI-3 and higher is supported
+ with this subsystem, but only for control CDBs capable by the device
+ firmware.
+
+ B{fileio}
+ ---------
+ This I{backstore_plugin} provides I{SPC-4}, along with I{ALUA} and
+ I{Persistent Reservations} emulation on top of Linux VFS devices:
+ B{any file on a mounted filesystem}. It may be backed by a file or an
+ underlying real block device. FILEIO is using struct file to serve
+ block I/O with various methods (synchronous or asynchronous) and
+ (buffered or direct).
+
+ B{rd_dr}
+ -------
+ This I{backstore_plugin} provides the same level of SCSI emulation than
+ the I{fileio} and I{iblock} backstores, but uses a B{ramdisk}, based on
+ direct memory mapping. It is the fastest of all backstores, and is
+ typically used for bandwidth testing.
+
+ B{rd_mcp}
+ --------
+ This I{backstore_plugin} is a bit slower than B{rd_dr}, but more robust
+ with multiple initiators, with a separate memory mapping using memory
+ copy. Also typically used for bandwidth testing.
+
+ EXAMPLE
+ =======
+
+ B{create iblock}
+ ----------------
+ Creates a new backstore, using the B{iblock} I{backstore_plugin}.
+ '''
+ self.log.debug("%r" % [(backstore.plugin, backstore.index)
+ for backstore in RTSRoot().backstores])
+ indexes = [backstore.index for backstore in RTSRoot().backstores
+ if backstore.plugin == backstore_plugin]
+ self.log.debug("Existing %s backstore indexes: %r"
+ % (backstore_plugin, indexes))
+ for index in range(1048576):
+ if index not in indexes:
+ backstore_index = index
+ break
+
+ if backstore_index is None:
+ self.log.error("Cannot find an available backstore index.")
+ return
+ else:
+ self.log.info("First available %s backstore index is %d."
+ % (backstore_plugin, backstore_index))
+
+ if backstore_plugin == 'pscsi':
+ backstore = PSCSIBackstore(backstore_index, mode='create')
+ self.add_child(UIPSCSIBackstore(backstore))
+ elif backstore_plugin == 'rd_dr':
+ backstore = RDDRBackstore(backstore_index, mode='create')
+ self.add_child(UIRDDRBackstore(backstore))
+ elif backstore_plugin == 'rd_mcp':
+ backstore = RDMCPBackstore(backstore_index, mode='create')
+ self.add_child(UIRDMCPBackstore(backstore))
+ elif backstore_plugin == 'fileio':
+ backstore = FileIOBackstore(backstore_index, mode='create')
+ self.add_child(UIFileIOBackstore(backstore))
+ elif backstore_plugin == 'iblock':
+ backstore = IBlockBackstore(backstore_index, mode='create')
+ self.add_child(UIIBlockBackstore(backstore))
+ else:
+ self.log.error("Invalid backstore plugin %s" % backstore_plugin)
+ return
+
+ self.log.info("Created new backstore %s" % backstore.name)
+
+ def ui_complete_create(self, parameters, text, current_param):
+ '''
+ Parameter auto-completion method for user command create.
+ @param parameters: Parameters on the command line.
+ @type parameters: dict
+ @param text: Current text of parameter being typed by the user.
+ @type text: str
+ @param current_param: Name of parameter to complete.
+ @type current_param: str
+ @return: Possible completions
+ @rtype: list of str
+ '''
+ if current_param == 'backstore_plugin':
+ plugins = ['pscsi', 'rd_dr', 'rd_mcp', 'fileio', 'iblock']
+ completions = [plugin for plugin in plugins
+ if plugin.startswith(text)]
+ else:
+ completions = []
+
+ if len(completions) == 1:
+ return [completions[0] + ' ']
+ else:
+ return completions
+
+ def ui_command_delete(self, backstore):
+ '''
+ Deletes a I{backstore}, and recursively all defined storage objects
+ hanging under it. If there are existing LUNs making use of those
+ storage objects, they will be deleted too.
+
+ EXAMPLE
+ =======
+ B{delete iblock2}
+ -----------------
+ That would recursively delete the B{iblock} backstore with index 2.
+ '''
+ for child in self.children:
+ if child.name == backstore:
+ child.backstore.delete()
+ self.del_child(child)
+ self.log.info("Deleted backstore %s." % backstore)
+ self.parent.refresh()
+ break
+ else:
+ self.log.error("Cannot find backstore to delete: %s."
+ % backstore)
+
+ def ui_complete_delete(self, parameters, text, current_param):
+ '''
+ Parameter auto-completion method for user command delete.
+ @param parameters: Parameters on the command line.
+ @type parameters: dict
+ @param text: Current text of parameter being typed by the user.
+ @type text: str
+ @param current_param: Name of parameter to complete.
+ @type current_param: str
+ @return: Possible completions
+ @rtype: list of str
+ '''
+ if current_param == 'backstore':
+ backstores = [child.name for child in self.children]
+ completions = [backstore for backstore in backstores
+ if backstore.startswith(text)]
+ else:
+ completions = []
+
+ if len(completions) == 1:
+ return [completions[0] + ' ']
+ else:
+ return completions
+
+class UIBackstore(UINode):
+ '''
+ A backstore UI.
+ '''
+ def __init__(self, backstore):
+ UINode.__init__(self)
+ self.backstore = backstore
+ self.name = backstore.name
+ self.cfs_cwd = backstore.path
+ self.refresh()
+
+ def refresh(self):
+ self._children = set([])
+ for storage_object in self.backstore.storage_objects:
+ self.add_child(UIStorageObject(storage_object))
+
+ def summary(self):
+ no_storage_objects = len(self._children)
+ if no_storage_objects > 1:
+ msg = "%d Storage Objects" % no_storage_objects
+ else:
+ msg = "%d Storage Object" % no_storage_objects
+ return (msg, None)
+
+ def use_wwn(self, wwn):
+ if wwn is None:
+ wwn == True
+ if wwn:
+ self.log.info("Generating a wwn serial.")
+ else:
+ self.log.info("Not generating a wwn serial.")
+ return wwn
+
+ def use_buffered(self, buffered):
+ if buffered is None:
+ buffered == True
+ if buffered:
+ self.log.info("Using buffered mode.")
+ else:
+ self.log.info("Not using buffered mode.")
+ return buffered
+
+ def ui_command_version(self):
+ '''
+ Displays the version of the current backstore's plugin.
+ '''
+ self.con.display("Backstore plugin %s %s"
+ % (self.backstore.plugin, self.backstore.version))
+
+ def ui_command_delete(self, name):
+ '''
+ Recursively deletes the storage object having the specified I{name}. If
+ there are LUNs using this storage object, they will be deleted too.
+
+ EXAMPLE
+ =======
+ B{delete mystorage}
+ -------------------
+ Deletes the storage object named mystorage, and all associated LUNs.
+ '''
+ for child in self.children:
+ if child.name == name:
+ child.storage_object.delete()
+ self.del_child(child)
+ self.log.info("Deleted storage object %s." % name)
+ self.parent.parent.refresh()
+ break
+ else:
+ self.log.error("Could not find storage object to delete: %s."
+ % name)
+
+ def ui_complete_delete(self, parameters, text, current_param):
+ '''
+ Parameter auto-completion method for user command delete.
+ @param parameters: Parameters on the command line.
+ @type parameters: dict
+ @param text: Current text of parameter being typed by the user.
+ @type text: str
+ @param current_param: Name of parameter to complete.
+ @type current_param: str
+ @return: Possible completions
+ @rtype: list of str
+ '''
+ if current_param == 'name':
+ names = [child.name for child in self.children]
+ completions = [name for name in names
+ if name.startswith(text)]
+ else:
+ completions = []
+
+ if len(completions) == 1:
+ return [completions[0] + ' ']
+ else:
+ return completions
+
+class UIPSCSIBackstore(UIBackstore):
+ '''
+ PSCSI backstore UI.
+ '''
+ def ui_command_create(self, name, dev):
+ '''
+ Creates a PSCSI storage object, with supplied name and SCSI device. The
+ SCSI device I{dev} can either be a path name to the device, in which
+ case it is recommended to use the /dev/disk/by-id hierarchy to have
+ consistent naming should your physical SCSI system be modified, or an
+ SCSI device ID in the H:C:T:L format, which is not recommended as SCSI
+ IDs may vary in time.
+ '''
+ so = PSCSIStorageObject(self.backstore, name, dev)
+ self.add_child(UIStorageObject(so))
+ self.log.info("Created pscsi storage object %s using %s"
+ % (name, dev))
+
+class UIRDDRBackstore(UIBackstore):
+ '''
+ RDDR backstore UI.
+ '''
+ def ui_command_create(self, name, size, wwn=None):
+ '''
+ Creates an RDDR storage object. I{size} is the size of the ramdisk, and
+ the optional I{wwn} parameter is a boolean specifying whether or not we
+ should generate a T10 wwn serial for the unit (by default, yes).
+
+ SIZE SYNTAX
+ ===========
+ - If size is an int, it represents a number of bytes.
+ - If size is a string, the following units can be used:
+ - B{B} or no unit present for bytes
+ - B{k}, B{K}, B{kB}, B{KB} for kB (kilobytes)
+ - B{m}, B{M}, B{mB}, B{MB} for MB (megabytes)
+ - B{g}, B{G}, B{gB}, B{GB} for GB (gigabytes)
+ - B{t}, B{T}, B{tB}, B{TB} for TB (terabytes)
+ '''
+ so = RDDRStorageObject(self.backstore, name, size, self.use_wwn(wwn))
+ self.add_child(UIStorageObject(so))
+ self.log.info("Created rd_dr ramdisk storage object %s with size %s."
+ % (name, size))
+
+class UIRDMCPBackstore(UIBackstore):
+ '''
+ RDMCP backstore UI.
+ '''
+ def ui_command_create(self, name, size, wwn=None):
+ '''
+ Creates an RDMCP storage object. I{size} is the size of the ramdisk,
+ and the optional I{wwn} parameter is a boolean specifying whether or
+ not we should generate a T10 wwn Serial for the unit (by default, yes).
+
+ SIZE SYNTAX
+ ===========
+ - If size is an int, it represents a number of bytes.
+ - If size is a string, the following units can be used:
+ - B{B} or no unit present for bytes
+ - B{k}, B{K}, B{kB}, B{KB} for kB (kilobytes)
+ - B{m}, B{M}, B{mB}, B{MB} for MB (megabytes)
+ - B{g}, B{G}, B{gB}, B{GB} for GB (gigabytes)
+ - B{t}, B{T}, B{tB}, B{TB} for TB (terabytes)
+ '''
+ so = RDMCPStorageObject(self.backstore, name, size, self.use_wwn(wwn))
+ self.add_child(UIStorageObject(so))
+ self.log.info("Created rd_mcp ramdisk storage object %s with size %s."
+ % (name, size))
+
+class UIFileIOBackstore(UIBackstore):
+ '''
+ FileIO backstore UI.
+ '''
+ def ui_command_create(self, name, file_or_dev, size=None, wwn=None,
+ buffered=None):
+ '''
+ Creates a FileIO storage object. If I{file_or_dev} is a path to a
+ regular file to be used as backend, then the I{size} parameter is
+ mandatory. Else, if I{file_or_dev} is a path to a block device, the
+ size parameter B{must} be ommited. If present, I{size} is the size of
+ the file to be used, I{file} the path to the file or I{dev} the path to
+ a block device. The optional I{wwn} parameter is a boolean specifying
+ whether or not we should generate a T10 wwn Serial for the unit (by
+ default, yes). The I{buffered} parameter is a boolean stating whether
+ or not to enable buffered mode. It is disabled by default (synchronous
+ mode).
+
+ SIZE SYNTAX
+ ===========
+ - If size is an int, it represents a number of bytes.
+ - If size is a string, the following units can be used:
+ - B{B} or no unit present for bytes
+ - B{k}, B{K}, B{kB}, B{KB} for kB (kilobytes)
+ - B{m}, B{M}, B{mB}, B{MB} for MB (megabytes)
+ - B{g}, B{G}, B{gB}, B{GB} for GB (gigabytes)
+ - B{t}, B{T}, B{tB}, B{TB} for TB (terabytes)
+ '''
+ is_dev = get_block_type(file_or_dev) is not None \
+ or is_disk_partition(file_or_dev)
+
+ if size is None and is_dev:
+ so = FileIOStorageObject(self.backstore, name, file_or_dev,
+ gen_wwn=self.use_wwn(wwn),
+ buffered_mode=self.use_buffered(buffered))
+ self.log.info("Created fileio storage object %s with size %s."
+ % (name, size))
+ self.add_child(UIStorageObject(so))
+ elif size is not None and not is_dev:
+ so = FileIOStorageObject(self.backstore, name, file_or_dev, size,
+ gen_wwn=self.use_wwn(wwn),
+ buffered_mode=self.use_buffered(buffered))
+ self.log.info("Created fileio storage object %s." % name)
+ self.add_child(UIStorageObject(so))
+ else:
+ self.log.error("For fileio, you must either specify both a file "
+ + "and a size, or just a device path.")
+
+class UIIBlockBackstore(UIBackstore):
+ '''
+ IBlock backstore UI.
+ '''
+ def ui_command_create(self, name, dev, wwn=None):
+ '''
+ Creates an IBlock Storage object. I{dev} is the path to the TYPE_DISK
+ block device to use and the optional I{wwn} parameter is a boolean
+ specifying whether or not we should generate a T10 wwn Serial for the
+ unit (by default, yes).
+ '''
+ if wwn is None:
+ wwn == True
+ if wwn:
+ self.log.info("Generating a wwn serial.")
+ else:
+ self.log.info("Not generating a wwn serial.")
+
+ so = IBlockStorageObject(self.backstore, name, dev, wwn)
+ self.add_child(UIStorageObject(so))
+ self.log.info("Created iblock storage object %s using %s."
+ % (name, dev))
+
+class UIStorageObject(UINode, UIAttributes):
+ '''
+ A storage object UI.
+ '''
+ def __init__(self, storage_object):
+ UINode.__init__(self)
+ UIAttributes.__init__(self, storage_object)
+ self.storage_object = storage_object
+ self.name = storage_object.name
+ self.cfs_cwd = storage_object.path
+ self.refresh()
+
+ def summary(self):
+ so = self.storage_object
+ if so.backstore.plugin.startswith("rd"):
+ path = "ramdisk"
+ else:
+ path = so.udev_path
+ if not path:
+ return ("BROKEN STORAGE LINK", False)
+ else:
+ return ("%s %s" % (path, so.status), True)
+
diff --git a/rtsadmin/ui_node.py b/rtsadmin/ui_node.py
new file mode 100644
index 0000000..3af0b7a
--- /dev/null
+++ b/rtsadmin/ui_node.py
@@ -0,0 +1,164 @@
+'''
+Implements the rtsadmin base UI node.
+
+This file is part of RTSAdmin Community Edition.
+Copyright (c) 2011 by RisingTide Systems LLC
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU Affero General Public License as
+published by the Free Software Foundation, version 3 (AGPLv3).
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU Affero General Public License for more details.
+
+You should have received a copy of the GNU Affero General Public License
+along with this program. If not, see <http://www.gnu.org/licenses/>.
+'''
+
+from os import system
+from configshell import ConfigNode
+from rtslib import RTSLibError, RTSRoot
+
+class UINode(ConfigNode):
+ '''
+ Our rtsadmin basic UI node.
+ '''
+ def __init__(self):
+ ConfigNode.__init__(self)
+ self.cfs_cwd = RTSRoot.configfs_dir
+ self._configuration_groups['global']['auto_enable_tpgt'] = \
+ [self.ui_type_onoff,
+ 'If on, automatically enables TPGTs upon creation.']
+
+ def ui_command_refresh(self):
+ '''
+ Refreshes and updates the objects tree from the current path.
+ '''
+ self.refresh()
+
+ def ui_command_saveconfig(self):
+ '''
+ Saves the whole configuration tree to disk so that it will be restored
+ on next boot. Unless you do that, changes are lost accross reboots.
+ '''
+ self.con.display("WARNING: Saving the current configuration to disk "
+ + "will overwrite your boot settings.")
+ self.con.display("The current target configuration will become the "
+ + "default boot config.")
+ system('PYTHONPATH="" python /usr/sbin/tcm_dump --o')
+
+ def refresh(self):
+ '''
+ Refreshes and updates the objects tree from the current path.
+ '''
+ for child in self.children:
+ child.refresh()
+
+ def execute_command(self, command, pparams=[], kparams={}):
+ '''
+ We overload this one in order to handle our own exceptions cleanly,
+ and not just configshell's ExecutionError.
+ '''
+ if command == '_cfs':
+ self.log.info(self.cfs_cwd)
+ return
+ elif command == '_sh':
+ self.log.info("Opening a shell in %s." % self.cfs_cwd)
+ self.con.display("Type [CTRL-D] or exit to come back.")
+ system("(cd %s; bash)" % self.cfs_cwd )
+ return
+ try:
+ result = ConfigNode.execute_command(self, command,
+ pparams, kparams)
+ except RTSLibError, msg:
+ self.log.error(msg)
+ else:
+ self.log.debug("Command %s succeeded." % command)
+ return result
+
+ def ui_command_status(self):
+ '''
+ Displays the current node's status summary.
+
+ SEE ALSO
+ ========
+ B{ls}
+ '''
+ description, is_healthy = self.summary()
+ self.log.info("Status for %s: %s" % (self.path, description))
+
+class UIParameters(object):
+ '''
+ A completely virtual class to implement UI methods for setting/getting
+ rtslib object parameters. Requires a set cfs_object parameter and calling
+ __init__(self, cfs_object, ).
+ '''
+ def __init__(self, rtslib_object):
+ '''
+ Call from the class that inherits this, with the rtslib object that
+ should be queried for parameters.
+ '''
+ self._rtslib_object = rtslib_object
+ self._configuration_groups['parameter'] = {}
+ for parameter in self._rtslib_object.list_parameters():
+ self._configuration_groups['parameter'][parameter] = \
+ [self.ui_type_string, "The %s parameter." % parameter]
+
+ def ui_getgroup_parameter(self, parameter):
+ '''
+ This is the backend method for getting parameters.
+ @param parameter: The parameter to get the value of.
+ @type parameter: str
+ @return: The parameter's value
+ @rtype: arbitrary
+ '''
+ return self._rtslib_object.get_parameter(parameter)
+
+ def ui_setgroup_parameter(self, parameter, value):
+ '''
+ This is the backend method for setting parameters.
+ @param parameter: The parameter to set the value of.
+ @type parameter: str
+ @param value: The parameter's value
+ @type value: arbitrary
+ '''
+ self._rtslib_object.set_parameter(parameter, value)
+
+class UIAttributes(object):
+ '''
+ A completely virtual class to implement UI methods for setting/getting
+ rtslib object attributes. Requires a set cfs_object attribute and calling
+ __init__(self, cfs_object, ).
+ '''
+ def __init__(self, rtslib_object):
+ '''
+ Call from the class that inherits this, with the rtslib object that
+ should be queried for attributes.
+ '''
+ self._rtslib_object = rtslib_object
+ self._configuration_groups['attribute'] = {}
+ for attribute in self._rtslib_object.list_attributes():
+ self._configuration_groups['attribute'][attribute] = \
+ [self.ui_type_string, "The %s attribute." % attribute]
+
+ def ui_getgroup_attribute(self, attribute):
+ '''
+ This is the backend method for getting attributes.
+ @param attribute: The attribute to get the value of.
+ @type attribute: str
+ @return: The attribute's value
+ @rtype: arbitrary
+ '''
+ return self._rtslib_object.get_attribute(attribute)
+
+ def ui_setgroup_attribute(self, attribute, value):
+ '''
+ This is the backend method for setting attributes.
+ @param attribute: The attribute to set the value of.
+ @type attribute: str
+ @param value: The attribute's value
+ @type value: arbitrary
+ '''
+ self._rtslib_object.set_attribute(attribute, value)
diff --git a/rtsadmin/ui_root.py b/rtsadmin/ui_root.py
new file mode 100644
index 0000000..40b1df9
--- /dev/null
+++ b/rtsadmin/ui_root.py
@@ -0,0 +1,78 @@
+'''
+Implements the rtsadmin root UI.
+
+This file is part of RTSAdmin Community Edition.
+Copyright (c) 2011 by RisingTide Systems LLC
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU Affero General Public License as
+published by the Free Software Foundation, version 3 (AGPLv3).
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU Affero General Public License for more details.
+
+You should have received a copy of the GNU Affero General Public License
+along with this program. If not, see <http://www.gnu.org/licenses/>.
+'''
+
+from rtslib import RTSRoot
+from ui_node import UINode
+from ui_target import UIFabricModule
+from ui_backstore import UIBackstores
+
+class UIRoot(UINode):
+ '''
+ The rtsadmin hierarchy root node.
+ '''
+ def __init__(self):
+ self.loaded = False
+ UINode.__init__(self)
+ self.name = '/'
+
+ def refresh(self):
+ '''
+ Refreshes the tree of target fabric modules.
+ '''
+ self._children = set([])
+ self.add_child(UIBackstores())
+ if not self.loaded:
+ self.log.debug("Refreshing in non-loaded mode.")
+ for fabric_module in RTSRoot().fabric_modules:
+ if fabric_module:
+ self.log.info("Using %s fabric module." \
+ % fabric_module.name)
+ self.add_child(UIFabricModule(fabric_module))
+ else:
+ try:
+ for step in fabric_module.load(yield_steps=True):
+ (action, taken, desc) = step
+ if taken:
+ self.log.info(desc)
+ self.log.info("Done loading %s fabric module." \
+ % fabric_module.name)
+ except Exception, msg:
+ self.log.warning("Can't load fabric module %s."
+ % fabric_module.name)
+ self.log.debug(msg)
+ else:
+ self.add_child(UIFabricModule(fabric_module))
+ self.loaded = True
+ else:
+ self.log.debug("Refreshing in loaded mode.")
+ for fabric_module in RTSRoot().loaded_fabric_modules:
+ self.log.debug("Loading %s." % fabric_module.name)
+ self.add_child(UIFabricModule(fabric_module))
+
+ def ui_command_version(self):
+ '''
+ Displays the rtsadmin and support libraries versions.
+ '''
+ from rtslib import __version__ as rtslib_version
+ from rtsadmin import __version__ as rtsadmin_version
+ from configshell import __version__ as configshell_version
+ self.log.info("Running rtsadmin version %s" % rtsadmin_version)
+ self.log.info("Using rtslib version %s" % rtslib_version)
+ self.log.info("Using configshell version %s" % configshell_version)
+
diff --git a/rtsadmin/ui_target.py b/rtsadmin/ui_target.py
new file mode 100644
index 0000000..f3e95dc
--- /dev/null
+++ b/rtsadmin/ui_target.py
@@ -0,0 +1,846 @@
+'''
+Implements the rtsadmin target related UI.
+
+This file is part of RTSAdmin Community Edition.
+Copyright (c) 2011 by RisingTide Systems LLC
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU Affero General Public License as
+published by the Free Software Foundation, version 3 (AGPLv3).
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU Affero General Public License for more details.
+
+You should have received a copy of the GNU Affero General Public License
+along with this program. If not, see <http://www.gnu.org/licenses/>.
+'''
+
+from ui_node import UINode, UIAttributes, UIParameters
+from rtslib import RTSLibError, RTSLibBrokenLink, utils
+from rtslib import NodeACL, NetworkPortal, MappedLUN
+from rtslib import RTSRoot, Target, TPG, LUN
+
+class UIFabricModule(UINode):
+ '''
+ A fabric module UI.
+ '''
+ def __init__(self, fabric_module):
+ UINode.__init__(self)
+ self.name = fabric_module.name
+ self.fabric_module = fabric_module
+ self.cfs_cwd = fabric_module.path
+ self.refresh()
+
+ def refresh(self):
+ self._children = set([])
+ for target in self.fabric_module.targets:
+ self.log.debug("Found target %s under fabric module %s."
+ % (target.wwn, target.fabric_module))
+ if target.has_feature('tpgts'):
+ self.add_child(UIMultiTPGTarget(target))
+ else:
+ self.add_child(UITarget(target))
+
+ def summary(self):
+ no_targets = len(self._children)
+ if no_targets > 1:
+ msg = "%d Targets" % no_targets
+ else:
+ msg = "%d Target" % no_targets
+ return (msg, None)
+
+ def ui_command_create(self, wwn=None):
+ '''
+ Creates a new target. The I{wwn} format depends on the transport(s)
+ supported by the fabric module. If the I{wwn} is ommited, then a
+ target will be created using either a randomly generated WWN of the
+ proper type, or the first unused WWN in the list of possible WWNs if
+ one is available. If WWNs are constrained to a list (i.e. for hardware
+ targets addresses) and all WWNs are in use, the target creation will
+ fail. Use the B{info} command to get more information abour WWN type
+ and possible values.
+
+ SEE ALSO
+ ========
+ B{info}
+ '''
+ target = Target(self.fabric_module, wwn, mode='create')
+ wwn = target.wwn
+ self.add_child(UITarget(target))
+ self.log.info("Created target %s." % wwn)
+
+ def ui_complete_create(self, parameters, text, current_param):
+ '''
+ Parameter auto-completion method for user command create.
+ @param parameters: Parameters on the command line.
+ @type parameters: dict
+ @param text: Current text of parameter being typed by the user.
+ @type text: str
+ @param current_param: Name of parameter to complete.
+ @type current_param: str
+ @return: Possible completions
+ @rtype: list of str
+ '''
+ spec = self.fabric_module.spec
+ if current_param == 'wwn' and spec['wwn_list'] is not None:
+ existing_wwns = [child.wwn for child in self.fabric_module.targets]
+ completions = [wwn for wwn in spec['wwn_list']
+ if wwn.startswith(text)
+ if wwn not in existing_wwns]
+ else:
+ completions = []
+
+ if len(completions) == 1:
+ return [completions[0] + ' ']
+ else:
+ return completions
+
+ def ui_command_delete(self, wwn):
+ '''
+ Recursively deletes the target with the specified I{wwn}, and all
+ objects hanging under it.
+
+ SEE ALSO
+ ========
+ B{create}
+ '''
+ target = Target(self.fabric_module, wwn, mode='lookup')
+ target.delete()
+ self.log.info("Deleted Target %s." % wwn)
+ self.refresh()
+
+ def ui_complete_delete(self, parameters, text, current_param):
+ '''
+ Parameter auto-completion method for user command delete.
+ @param parameters: Parameters on the command line.
+ @type parameters: dict
+ @param text: Current text of parameter being typed by the user.
+ @type text: str
+ @param current_param: Name of parameter to complete.
+ @type current_param: str
+ @return: Possible completions
+ @rtype: list of str
+ '''
+ if current_param == 'wwn':
+ wwns = [child.name for child in self.children]
+ completions = [wwn for wwn in wwns if wwn.startswith(text)]
+ else:
+ completions = []
+
+ if len(completions) == 1:
+ return [completions[0] + ' ']
+ else:
+ return completions
+
+ def ui_command_info(self):
+ '''
+ Displays information about the fabric module, notably the supported
+ transports(s) and accepted B{wwn} format(s), as long as supported
+ features.
+ '''
+ spec = self.fabric_module.spec
+ self.log.info("Fabric module name: %s"
+ % self.name)
+ self.log.info("ConfigFS path: %s"
+ % self.fabric_module.path)
+ if spec['wwn_list'] is not None:
+ self.log.info("Allowed WWNs list (%s type): %s"
+ % (spec['wwn_type'], ', '.join(spec['wwn_list'])))
+ else:
+ self.log.info("Supported WWN type: %s"
+ % spec['wwn_type'])
+ self.log.info("Fabric module specfile: %s"
+ % self.fabric_module.spec_file)
+ self.log.info("Fabric module features: %s"
+ % ', '.join(spec['features']))
+ self.log.info("Corresponding kernel module: %s"
+ % spec['kernel_module'])
+
+ def ui_command_version(self):
+ '''
+ Displays the target fabric module version.
+ '''
+ version = "Target fabric module %s: %s" \
+ % (self.fabric_module.name, self.fabric_module.version)
+ self.con.display(version.strip())
+
+class UIMultiTPGTarget(UINode):
+ '''
+ A generic target UI that has multiple TPGs.
+ '''
+ def __init__(self, target):
+ UINode.__init__(self)
+ self.target = target
+ self.name = target.wwn
+ self.cfs_cwd = target.path
+ self.refresh()
+
+ def refresh(self):
+ self._children = set([])
+ for tpg in self.target.tpgs:
+ self.add_child(UITPG(tpg))
+
+ def summary(self):
+ if not self.target.fabric_module.is_valid_wwn(self.target.wwn):
+ description = "INVALID WWN"
+ is_healthy = False
+ else:
+ is_healthy = None
+ no_tpgs = len(self._children)
+ if no_tpgs > 1:
+ description = "%d TPGs" % no_tpgs
+ else:
+ description = "%d TPG" % no_tpgs
+
+ return (description, is_healthy)
+
+ def ui_command_create(self, tag=None):
+ '''
+ Creates a new Target Portal Group within the target. The I{tag} must be
+ a strictly positive integer value. If omitted, the next available
+ Target Portal Group Tag (TPGT) will be used.
+
+ SEE ALSO
+ ========
+ B{delete}
+ '''
+ if tag is None:
+ tags = [tpg.tag for tpg in self.target.tpgs]
+ for index in range(1048576):
+ if index not in tags and index > 0:
+ tag = index
+ break
+ if tag is None:
+ self.log.error("Cannot find an available TPG Tag.")
+ return
+ else:
+ self.log.info("Selected TPG Tag %d." % tag)
+ else:
+ try:
+ tag = int(tag)
+ except ValueError:
+ self.log.error("The TPG Tag must be an integer value.")
+ return
+ else:
+ if tag < 1:
+ self.log.error("The TPG Tag must be >0.")
+ return
+
+ tpg = TPG(self.target, tag, mode='create')
+ if self.prefs['auto_enable_tpgt']:
+ tpg.enable = True
+ self.log.info("Successfully created TPG %s." % tpg.tag)
+ self.add_child(UITPG(tpg))
+
+ def ui_command_delete(self, tag):
+ '''
+ Deletes the Target Portal Group with TPGT I{tag} from the target. The
+ I{tag} must be a positive integer matching an existing TPGT.
+
+ SEE ALSO
+ ========
+ B{create}
+ '''
+ tpg = TPG(self.target, tag, mode='lookup')
+ tpg.delete()
+ self.log.info("Deleted TPGT %s." % tag)
+ self.refresh()
+
+ def ui_complete_delete(self, parameters, text, current_param):
+ '''
+ Parameter auto-completion method for user command delete.
+ @param parameters: Parameters on the command line.
+ @type parameters: dict
+ @param text: Current text of parameter being typed by the user.
+ @type text: str
+ @param current_param: Name of parameter to complete.
+ @type current_param: str
+ @return: Possible completions
+ @rtype: list of str
+ '''
+ if current_param == 'tag':
+ tags = [child.name[4:] for child in self.children]
+ completions = [tag for tag in tags if tag.startswith(text)]
+ else:
+ completions = []
+
+ if len(completions) == 1:
+ return [completions[0] + ' ']
+ else:
+ return completions
+
+class UITPG(UINode, UIAttributes, UIParameters):
+ '''
+ A generic TPG UI.
+ '''
+ def __init__(self, tpg):
+ UINode.__init__(self)
+ UIAttributes.__init__(self, tpg)
+ UIParameters.__init__(self, tpg)
+ self.tpg = tpg
+ self.name = "tpgt%d" % tpg.tag
+ self.cfs_cwd = tpg.path
+ self.refresh()
+
+ self.add_child(UILUNs(tpg))
+
+ if tpg.has_feature('acls'):
+ self.add_child(UINodeACLs(self.tpg))
+ if tpg.has_feature('nps'):
+ self.add_child(UIPortals(self.tpg))
+
+ def summary(self):
+ if self.tpg.has_feature('nexus'):
+ description = "%s" % self.tpg.nexus
+ elif self.tpg.enable:
+ description = "enabled"
+ else:
+ description = "disabled"
+ return (description, True)
+
+ def ui_command_enable(self):
+ '''
+ Enables the TPG.
+
+ SEE ALSO
+ ========
+ B{disable status}
+ '''
+ if self.tpg.enable:
+ self.log.info("The TPGT is already enabled.")
+ else:
+ self.tpg.enable = True
+ self.log.info("The TPGT has been enabled.")
+
+ def ui_command_disable(self):
+ '''
+ Disables the TPG.
+
+ SEE ALSO
+ ========
+ B{enable status}
+ '''
+ if self.tpg.enable:
+ self.tpg.enable = False
+ self.log.info("The TPGT has been disabled.")
+ else:
+ self.log.info("The TPGT is already disabled.")
+
+class UITarget(UITPG):
+ '''
+ A generic target UI merged with its only TPG.
+ '''
+ # FIXME: This is a workaround for rtslib/kernel-side LIO enforcing at least
+ # one TPG layer even for non-iSCSI fabric modules, for legacy reasons.
+ def __init__(self, target):
+ UITPG.__init__(self, TPG(target, 1))
+ self.target = target
+ self.tpg.enable = True
+ self.name = target.wwn
+
+ def summary(self):
+ if not self.target.fabric_module.is_valid_wwn(self.target.wwn):
+ return ("INVALID WWN", False)
+ else:
+ return UITPG.summary(self)
+
+
+class UINodeACLs(UINode):
+ '''
+ A generic UI for node ACLs.
+ '''
+ def __init__(self, tpg):
+ UINode.__init__(self)
+ self.name = "acls"
+ self.tpg = tpg
+ self.cfs_cwd = "%s/acls" % tpg.path
+ self.refresh()
+
+ def refresh(self):
+ self._children = set([])
+ for node_acl in self.tpg.node_acls:
+ self.add_child(UINodeACL(node_acl))
+
+ def summary(self):
+ no_acls = len(self._children)
+ if no_acls > 1:
+ msg = "%d ACLs" % no_acls
+ else:
+ msg = "%d ACL" % no_acls
+ return (msg, None)
+
+ def ui_command_create(self, wwn):
+ '''
+ Creates a Node ACL for the initiator node with the specified I{wwn}.
+ The node's I{wwn} must match the expected WWN Type of the target's
+ fabric module.
+
+ SEE ALSO
+ ========
+ B{delete}
+ '''
+ spec = self.tpg.parent_target.fabric_module.spec
+ if not utils.is_valid_wwn(spec['wwn_type'], wwn):
+ self.log.error("'%s' is not a valid %s WWN."
+ % (wwn, spec['wwn_type']))
+ return
+ try:
+ node_acl = NodeACL(self.tpg, wwn, mode="create")
+ except RTSLibError, msg:
+ self.log.error(msg)
+ return
+ else:
+ self.log.info("Successfully created Node ACL for %s"
+ % node_acl.node_wwn)
+ self.add_child(UINodeACL(node_acl))
+
+ def ui_command_delete(self, wwn):
+ '''
+ Deletes the Node ACL with the specified I{wwn}.
+
+ SEE ALSO
+ ========
+ B{create}
+ '''
+ node_acl = NodeACL(self.tpg, wwn, mode='lookup')
+ node_acl.delete()
+ self.log.info("Successfully deleted Node ACL %s." % wwn)
+ self.refresh()
+
+ def ui_complete_delete(self, parameters, text, current_param):
+ '''
+ Parameter auto-completion method for user command delete.
+ @param parameters: Parameters on the command line.
+ @type parameters: dict
+ @param text: Current text of parameter being typed by the user.
+ @type text: str
+ @param current_param: Name of parameter to complete.
+ @type current_param: str
+ @return: Possible completions
+ @rtype: list of str
+ '''
+ if current_param == 'wwn':
+ wwns = [acl.node_wwn for acl in self.tpg.node_acls]
+ completions = [wwn for wwn in wwns if wwn.startswith(text)]
+ else:
+ completions = []
+
+ if len(completions) == 1:
+ return [completions[0] + ' ']
+ else:
+ return completions
+
+class UINodeACL(UINode, UIAttributes, UIParameters):
+ '''
+ A generic UI for a node ACL.
+ '''
+ def __init__(self, node_acl):
+ UINode.__init__(self)
+ UIAttributes.__init__(self, node_acl)
+ UIParameters.__init__(self, node_acl)
+ self.name = node_acl.node_wwn
+ self.node_acl = node_acl
+ self.cfs_cwd = node_acl.path
+ self.refresh()
+
+ def refresh(self):
+ self._children = set([])
+ for mlun in self.node_acl.mapped_luns:
+ self.add_child(UIMappedLUN(mlun))
+
+ def summary(self):
+ no_mluns = len(self._children)
+ if no_mluns > 1:
+ msg = "%d Mapped LUNs" % no_mluns
+ else:
+ msg = "%d Mapped LUN" % no_mluns
+ return (msg, None)
+
+ def ui_command_create(self, mapped_lun, tpg_lun, write_protect=None):
+ '''
+ Creates a mapping to one of the TPG LUNs for the initiator referenced
+ by the ACL. The provided I{tpg_lun} will appear to that initiator as
+ LUN I{mapped_lun}. If the I{write_protect} flag is set to B{1}, the
+ initiator will not have write access to the Mapped LUN.
+
+ SEE ALSO
+ ========
+ B{delete}
+ '''
+ try:
+ tpg_lun = int(tpg_lun)
+ mapped_lun = int(mapped_lun)
+ except ValueError:
+ self.log.error("Incorrect LUN value.")
+ return
+
+ mlun = MappedLUN(self.node_acl, mapped_lun, tpg_lun, write_protect)
+ self.add_child(UIMappedLUN(mlun))
+ self.log.info("Created Mapped LUN %s." % mlun.mapped_lun)
+
+ def ui_command_delete(self, mapped_lun):
+ '''
+ Deletes the specified I{mapped_lun}.
+
+ SEE ALSO
+ ========
+ B{create}
+ '''
+ mlun = MappedLUN(self.node_acl, mapped_lun)
+ mlun.delete()
+ self.log.info("Deleted Mapped LUN %s." % mapped_lun)
+ self.refresh()
+
+class UIMappedLUN(UINode):
+ '''
+ A generic UI for MappedLUN objects.
+ '''
+ def __init__(self, mapped_lun):
+ UINode.__init__(self)
+ self.mapped_lun = mapped_lun
+ self.name = "mapped_lun%d" % mapped_lun.mapped_lun
+ self.cfs_cwd = mapped_lun.path
+ self.refresh()
+
+ def summary(self):
+ mapped_lun = self.mapped_lun
+ is_healthy = True
+ try:
+ tpg_lun = mapped_lun.tpg_lun
+ except RTSLibBrokenLink:
+ description = "BROKEN LUN LINK"
+ is_healthy = False
+ else:
+ if mapped_lun.write_protect:
+ access_mode = 'ro'
+ else:
+ access_mode = 'rw'
+ description = "lun%d (%s)" % (tpg_lun.lun, access_mode)
+
+ return (description, is_healthy)
+
+class UILUNs(UINode):
+ '''
+ A generic UI for TPG LUNs.
+ '''
+ def __init__(self, tpg):
+ UINode.__init__(self)
+ self.name = "luns"
+ self.cfs_cwd = "%s/lun" % tpg.path
+ self.tpg = tpg
+ self.refresh()
+
+ def refresh(self):
+ self._children = set([])
+ for lun in self.tpg.luns:
+ self.add_child(UILUN(lun))
+
+ def summary(self):
+ no_luns = len(self._children)
+ if no_luns > 1:
+ msg = "%d LUNs" % no_luns
+ else:
+ msg = "%d LUN" % no_luns
+ return (msg, None)
+
+ def ui_command_create(self, storage_object, lun=None):
+ '''
+ Creates a new LUN in the Target Portal Group, attached to a storage
+ object. If the I{lun} parameter is omitted, the first available LUN in
+ the TPG will be used. The I{storage_object} must be the path of an
+ existing storage object, i.e. B{/backstore/pscsi0/mydisk} to reference
+ the B{mydisk} storage object of the virtual HBA B{pscsi0}.
+
+ SEE ALSO
+ ========
+ B{delete}
+ '''
+ if lun is None:
+ luns = [lun.lun for lun in self.tpg.luns]
+ for index in range(1048576):
+ if index not in luns:
+ lun = index
+ break
+ if lun is None:
+ self.log.error("Cannot find an available LUN.")
+ return
+ else:
+ self.log.info("Selected LUN %d." % lun)
+ else:
+ try:
+ lun = int(lun)
+ except ValueError:
+ self.log.error("The LUN must be an integer value.")
+ return
+ else:
+ if lun < 0:
+ self.log.error("The LUN cannot be negative.")
+ return
+
+ try:
+ storage_object = self.get_node(storage_object).storage_object
+ except AttributeError:
+ self.log.error("Wrong storage object path.")
+
+ lun_object = LUN(self.tpg, lun, storage_object)
+ self.log.info("Successfully created LUN %s." % lun_object.lun)
+ self.add_child(UILUN(lun_object))
+
+ def ui_complete_create(self, parameters, text, current_param):
+ '''
+ Parameter auto-completion method for user command create.
+ @param parameters: Parameters on the command line.
+ @type parameters: dict
+ @param text: Current text of parameter being typed by the user.
+ @type text: str
+ @param current_param: Name of parameter to complete.
+ @type current_param: str
+ @return: Possible completions
+ @rtype: list of str
+ '''
+ if current_param == 'storage_object':
+ storage_objects = []
+ for backstore in self.get_node('/backstores').children:
+ for storage_object in backstore.children:
+ storage_objects.append(storage_object.path)
+ completions = [so for so in storage_objects if so.startswith(text)]
+ else:
+ completions = []
+
+ if len(completions) == 1:
+ return [completions[0] + ' ']
+ else:
+ return completions
+
+ def ui_command_delete(self, lun):
+ '''
+ Deletes the supplied LUN from the Target Portal Group. The I{lun} must
+ be a positive number matching an existing LUN.
+
+ SEE ALSO
+ ========
+ B{create}
+ '''
+ lun_object = LUN(self.tpg, lun)
+ lun_object.delete()
+ self.log.info("Successfully deleted LUN %s." % lun)
+ self.refresh()
+
+ def ui_complete_delete(self, parameters, text, current_param):
+ '''
+ Parameter auto-completion method for user command delete.
+ @param parameters: Parameters on the command line.
+ @type parameters: dict
+ @param text: Current text of parameter being typed by the user.
+ @type text: str
+ @param current_param: Name of parameter to complete.
+ @type current_param: str
+ @return: Possible completions
+ @rtype: list of str
+ '''
+ if current_param == 'lun':
+ luns = [str(lun.lun) for lun in self.tpg.luns]
+ completions = [lun for lun in luns if lun.startswith(text)]
+ else:
+ completions = []
+
+ if len(completions) == 1:
+ return [completions[0] + ' ']
+ else:
+ return completions
+
+class UILUN(UINode):
+ '''
+ A generic UI for LUN objects.
+ '''
+ def __init__(self, lun):
+ UINode.__init__(self)
+ self.lun = lun
+ self.name = "lun%d" % lun.lun
+ self.cfs_cwd = lun.path
+ self.refresh()
+
+ def summary(self):
+ lun = self.lun
+ is_healthy = True
+ try:
+ storage_object = lun.storage_object
+ except RTSLibBrokenLink:
+ description = "BROKEN STORAGE LINK"
+ is_healthy = False
+ else:
+ backstore = storage_object.backstore
+ if backstore.plugin.startswith("rd"):
+ path = "ramdisk"
+ else:
+ path = storage_object.udev_path
+ description = "%s%s/%s (%s)" % (backstore.plugin, backstore.index,
+ storage_object.name, path)
+
+ return (description, is_healthy)
+
+class UIPortals(UINode):
+ '''
+ A generic UI for TPG network portals.
+ '''
+ def __init__(self, tpg):
+ UINode.__init__(self)
+ self.tpg = tpg
+ self.name = "portals"
+ self.cfs_cwd = "%s/np" % tpg.path
+ self.refresh()
+
+ def refresh(self):
+ self._children = set([])
+ for portal in self.tpg.network_portals:
+ self.add_child(UIPortal(portal))
+
+ def summary(self):
+ no_portals = len(self._children)
+ if no_portals > 1:
+ msg = "%d Portals" % no_portals
+ else:
+ msg = "%d Portal" % no_portals
+ return (msg, None)
+
+ def ui_command_create(self, ip_address=None, ip_port=None):
+ '''
+ Creates a Network Portal with specified I{ip_address} and I{ip_port}.
+ If I{ip_port} is omitted, the default port for the target fabric will
+ be used. If I{ip_address} is omitted, the first IP address found
+ matching the local hostname will be used.
+
+ SEE ALSO
+ ========
+ B{delete}
+ '''
+ if ip_port is None:
+ # FIXME: Add a specfile parameter to determine that
+ ip_port = 3260
+ self.log.info("Using default IP port %d" % ip_port)
+ if ip_address is None:
+ if not ip_address:
+ ip_address = utils.get_main_ip()
+ if ip_address:
+ self.log.info("Automatically selected IP address %s."
+ % ip_address)
+ else:
+ self.log.error("Cannot find a usable IP address to "
+ + "create the Network Portal, aborting.")
+ return
+ elif ip_address not in utils.list_eth_ips():
+ self.log.error("Provided IP address %s does not exist, aborting."
+ % ip_address)
+ return
+
+ try:
+ ip_port = int(ip_port)
+ except ValueError:
+ self.log.error("The ip_port must be an integer value.")
+ return
+
+ portal = NetworkPortal(self.tpg, ip_address,
+ ip_port, mode='create')
+ self.log.info("Successfully created network portal %s:%d."
+ % (ip_address, ip_port))
+ self.add_child(UIPortal(portal))
+
+ def ui_complete_create(self, parameters, text, current_param):
+ '''
+ Parameter auto-completion method for user command create.
+ @param parameters: Parameters on the command line.
+ @type parameters: dict
+ @param text: Current text of parameter being typed by the user.
+ @type text: str
+ @param current_param: Name of parameter to complete.
+ @type current_param: str
+ @return: Possible completions
+ @rtype: list of str
+ '''
+ if current_param == 'ip_address':
+ completions = [addr for addr in utils.list_eth_ips()
+ if addr.startswith(text)]
+ else:
+ completions = []
+
+ if len(completions) == 1:
+ return [completions[0] + ' ']
+ else:
+ return completions
+
+ def ui_command_delete(self, ip_address, ip_port):
+ '''
+ Deletes the Network Portal with specified I{ip_address} and I{ip_port}.
+
+ SEE ALSO
+ ========
+ B{create}
+ '''
+ portal = NetworkPortal(self.tpg, ip_address, ip_port, mode='lookup')
+ portal.delete()
+ self.log.info("Deleted network portal %s:%s" % (ip_address, ip_port))
+ self.refresh()
+
+ def ui_complete_delete(self, parameters, text, current_param):
+ '''
+ Parameter auto-completion method for user command delete.
+ @param parameters: Parameters on the command line.
+ @type parameters: dict
+ @param text: Current text of parameter being typed by the user.
+ @type text: str
+ @param current_param: Name of parameter to complete.
+ @type current_param: str
+ @return: Possible completions
+ @rtype: list of str
+ '''
+ completions = []
+ # TODO: Check if a dict comprehension is acceptable here with supported
+ # XXX: python versions.
+ portals = {}
+ all_ports = set([])
+ for portal in self.tpg.network_portals:
+ all_ports.add(str(portal.port))
+ if not portal.ip_address in portals:
+ portals[portal.ip_address] = []
+ portals[portal.ip_address].append(str(portal.port))
+
+ if current_param == 'ip_address':
+ if 'ip_port' in parameters:
+ port = parameters['ip_port']
+ completions = [addr for addr in portals
+ if port in portals[addr]
+ if addr.startswith(text)]
+ else:
+ completions = [addr for addr in portals
+ if addr.startswith(text)]
+ elif current_param == 'ip_port':
+ if 'ip_address' in parameters:
+ addr = parameters['ip_address']
+ if addr in portals:
+ completions = [port for port in portals[addr]
+ if port.startswith(text)]
+ else:
+ completions = [port for port in all_ports
+ if port.startswith(text)]
+
+ if len(completions) == 1:
+ return [completions[0] + ' ']
+ else:
+ return completions
+
+class UIPortal(UINode):
+ '''
+ A generic UI for a network portal.
+ '''
+ def __init__(self, portal):
+ UINode.__init__(self)
+ self.portal = portal
+ self.name = "%s:%s" % (portal.ip_address, portal.port)
+ self.cfs_cwd = portal.path
+ self.refresh()
+
+ def summary(self):
+ return ('', True)
+
diff --git a/scripts/rtsadmin b/scripts/rtsadmin
new file mode 100755
index 0000000..422224d
--- /dev/null
+++ b/scripts/rtsadmin
@@ -0,0 +1,61 @@
+#!/usr/bin/python
+'''
+Starts the rtsadmin CLI shell.
+
+This file is part of RTSAdmin Community Edition.
+Copyright (c) 2011 by RisingTide Systems LLC
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU Affero General Public License as
+published by the Free Software Foundation, version 3 (AGPLv3).
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU Affero General Public License for more details.
+
+You should have received a copy of the GNU Affero General Public License
+along with this program. If not, see <http://www.gnu.org/licenses/>.
+Copyright (c) 2011 by RisingTide Systems LLC.
+All rights reserved.
+'''
+
+from rtsadmin import UIRoot
+from configshell import ConfigShell
+
+class RTSAdmin(ConfigShell):
+ default_prefs = {'color_path': 'magenta',
+ 'color_command': 'cyan',
+ 'color_parameter': 'magenta',
+ 'color_keyword': 'cyan',
+ 'completions_in_columns': True,
+ 'logfile': None,
+ 'loglevel_console': 'info',
+ 'loglevel_file': 'debug9',
+ 'color_mode': True,
+ 'prompt_length': 30,
+ 'tree_max_depth': 0,
+ 'tree_status_mode': True,
+ 'tree_round_nodes': True,
+ 'tree_show_root': True,
+ 'auto_enable_tpgt': True
+ }
+
+def main():
+ '''
+ Start the rtsadmin shell.
+ '''
+ root_node = UIRoot()
+ shell = RTSAdmin(root_node, '~/.rtsadmin')
+
+ shell.con.epy_write('Welcome to the B{RTSAdmin Community Edition} CLI.')
+ shell.con.display('')
+ shell.con.epy_write('Copyright (c) 2011 by RisingTide Systems LLC.')
+ shell.con.epy_write('Visit us at U{http://www.risingtidesystems.com}.')
+ shell.con.display('')
+
+ root_node.refresh()
+ shell.run_interactive()
+
+if __name__ == "__main__":
+ main()
diff --git a/setup.py b/setup.py
new file mode 100755
index 0000000..7968530
--- /dev/null
+++ b/setup.py
@@ -0,0 +1,43 @@
+#! /usr/bin/env python
+
+'''
+This file is part of RTSAdmin Community Edition.
+Copyright (c) 2011 by RisingTide Systems LLC
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU Affero General Public License as
+published by the Free Software Foundation, version 3 (AGPLv3).
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU Affero General Public License for more details.
+
+You should have received a copy of the GNU Affero General Public License
+along with this program. If not, see <http://www.gnu.org/licenses/>.
+'''
+
+import re
+from distutils.core import setup
+import rtsadmin
+
+PKG = rtsadmin
+VERSION = str(PKG.__version__)
+(AUTHOR, EMAIL) = re.match('^(.*?)\s*<(.*)>$', PKG.__author__).groups()
+URL = PKG.__url__
+LICENSE = PKG.__license__
+SCRIPTS = ["scripts/rtsadmin"]
+DESCRIPTION = PKG.__description__
+
+setup(
+ name=PKG.__name__,
+ description=DESCRIPTION,
+ version=VERSION,
+ author=AUTHOR,
+ author_email=EMAIL,
+ license=LICENSE,
+ url=URL,
+ scripts=SCRIPTS,
+ packages=[PKG.__name__],
+ package_data = {'':[]}
+ )