--- /dev/null
+Copyright (C) 2007-2008 King County Library System
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU 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 General Public License for more details.
+
--- /dev/null
+ GNU GENERAL PUBLIC LICENSE
+ Version 3, 29 June 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 General Public License is a free, copyleft license for
+software and other kinds of works.
+
+ The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works. By contrast,
+the GNU General Public License is 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. We, the Free Software Foundation, use the
+GNU General Public License for most of our software; it applies also to
+any other work released this way by its authors. You can apply it to
+your programs, too.
+
+ 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.
+
+ To protect your rights, we need to prevent others from denying you
+these rights or asking you to surrender the rights. Therefore, you have
+certain responsibilities if you distribute copies of the software, or if
+you modify it: responsibilities to respect the freedom of others.
+
+ For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must pass on to the recipients the same
+freedoms that you received. You must make sure that they, too, receive
+or can get the source code. And you must show them these terms so they
+know their rights.
+
+ Developers that use the GNU GPL protect your rights with two steps:
+(1) assert copyright on the software, and (2) offer you this License
+giving you legal permission to copy, distribute and/or modify it.
+
+ For the developers' and authors' protection, the GPL clearly explains
+that there is no warranty for this free software. For both users' and
+authors' sake, the GPL requires that modified versions be marked as
+changed, so that their problems will not be attributed erroneously to
+authors of previous versions.
+
+ Some devices are designed to deny users access to install or run
+modified versions of the software inside them, although the manufacturer
+can do so. This is fundamentally incompatible with the aim of
+protecting users' freedom to change the software. The systematic
+pattern of such abuse occurs in the area of products for individuals to
+use, which is precisely where it is most unacceptable. Therefore, we
+have designed this version of the GPL to prohibit the practice for those
+products. If such problems arise substantially in other domains, we
+stand ready to extend this provision to those domains in future versions
+of the GPL, as needed to protect the freedom of users.
+
+ Finally, every program is threatened constantly by software patents.
+States should not allow patents to restrict development and use of
+software on general-purpose computers, but in those that do, we wish to
+avoid the special danger that patents applied to a free program could
+make it effectively proprietary. To prevent this, the GPL assures that
+patents cannot be used to render the program non-free.
+
+ 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 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. Use with the GNU Affero General Public License.
+
+ 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 Affero 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 special requirements of the GNU Affero General Public License,
+section 13, concerning interaction through a network will apply to the
+combination as such.
+
+ 14. Revised Versions of this License.
+
+ The Free Software Foundation may publish revised and/or new versions of
+the GNU 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 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 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 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 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 General Public License for more details.
+
+ You should have received a copy of the GNU 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 the program does terminal interaction, make it output a short
+notice like this when it starts in an interactive mode:
+
+ <program> Copyright (C) <year> <name of author>
+ This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+ This is free software, and you are welcome to redistribute it
+ under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License. Of course, your program's commands
+might be different; for a GUI interface, you would use an "about box".
+
+ 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 GPL, see
+<http://www.gnu.org/licenses/>.
+
+ The GNU General Public License does not permit incorporating your program
+into proprietary programs. If your program is a subroutine library, you
+may consider it more useful to permit linking proprietary applications with
+the library. If this is what you want to do, use the GNU Lesser General
+Public License instead of this License. But first, please read
+<http://www.gnu.org/philosophy/why-not-lgpl.html>.
--- /dev/null
+
+ Constrictor
+
+ What is it?
+ ----------
+
+ Constrictor is a general purpose, distributed, multi-threaded load testing framework.
+
+ Project Site and Documentation
+ ------------------------------
+
+ http://svn.open-ils.org/trac/ILS-Contrib/wiki/Constrictor
+
+ Requirements
+ -----------
+
+ Python 2.4 or higher
+ pysqlite2 (for sqlite3 : http://www.initd.org/tracker/pysqlite/wiki/pysqlite)
+
+ For the Evergreen contrib module:
+ Python simplejson
+ OpenSRF Python libs
+ Evergreen Pythong libs
+
+
--- /dev/null
+# ---- Constrictor Properties -------------------------------------
+
+# script to run by default
+constrictor.script=sleep.py
+#constrictor.script=eg_fetch_user_groups.py
+#constrictor.script=eg_checkout_roundtrip.py
+#constrictor.script=eg_title_hold.py
+
+# comma separated list of directories where test scripts are found
+constrictor.scriptDirs=samples,contrib/evergreen
+
+# local directory to cache test-specific files
+constrictor.cacheDir=cache
+
+# default number of threads
+constrictor.numThreads=3
+
+# default number of iterations-per-thread
+constrictor.numIterations=1
+
+# sqlite database filename
+constrictor.dbFile=constrictor.db
+
+# IP address to listen on when waiting on controller commands
+constrictor.listenAddress=''
+
+# port to listen on when waiting on controller commands
+constrictor.port=21800
+
+# if true, constrictor will wait for commands from the controller (dashboard)
+#constrictor.listen=true
+
+#logs to stdout and stderr. options are 0=none,1=error,2=info,3=debug
+constrictor.loglevel=2
+
+
+
+
+# ---- Properties for the Evergreen contrib module --------------
+
+# Where on the server can we find the latest IDL file
+evergreen.IDLPath=/reports/fm_IDL.xml
+
+# the server to test
+evergreen.server=dev.gapines.org
+
+# defines the HTTP gateway path where queries are sent
+evergreen.gatewayPath=osrf-gateway-v1
+
+# defines the HTTP gateway data format
+#evergreen.netProtocol=XML
+evergreen.netProtocol=JSON
+
+# if true, we will automatically login at script load time. Set to tru
+# if any actions in the script require authentication
+evergreen.autologin=true
+
+# username, password, and workstation to login with.
+# this is the user whose permissions will be checked
+# when performing any action that requires authentication
+evergreen.username=demo
+evergreen.password=demo123
+evergreen.workstation=demo
+#evergreen.titleIDs=
+#evergreen.patronIDs=
+#evergreen.orgIDs=
+#evergreen.copyBarcodes=
+#evergreen.patronBarcodes=
+#evergreen.osrfConfig=
+#evergreen.osrfConfigContext=
--- /dev/null
+#!/usr/bin/python
+# -----------------------------------------------------------------------
+# Copyright (C) 2007-2008 King County Library System
+# Bill Erickson <erickson@esilibrary.com>
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU 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 General Public License for more details.
+# -----------------------------------------------------------------------
+
+import sys, getopt, os, errno
+from constrictor.properties import Properties
+from constrictor.db import DBConnection
+from constrictor.controller import DroneController
+from constrictor.script import ScriptThread, ScriptManager
+from constrictor.log import *
+from constrictor.utils import loadProps, saveProps, initDirs, initDB, openScript, PROPS_FILENAME
+
+props = None
+droneController = None
+
+def usage():
+ print '''
+python %s [options]
+
+ By default, all options are read from the properties file constrictor.properties.
+ Arguments passed via the command line will override any properties
+ loaded from the properties file
+
+ Options:
+ -h show this help message
+ -s test script to run (property constrictor.script)
+ -t number of threads to launch (property constrictor.numThreads)
+ -i number of test iterations per thread (property constrictor.numIterations)
+ -d database file (constrictor.property dbFile)
+ -p port to listen for controller connections on
+ -l listen address for incoming controller connections
+''' % sys.argv[0]
+ sys.exit(0)
+
+
+
+def onThreadsComplete(scriptManager):
+ global droneController
+ summary = ScriptThread.currentScriptThread().dbConnection.createTaskSummary()
+ droneController.sendResult(type='task_summary', **summary)
+
+
+def readArgv():
+ # see if we have any command-line args that override the properties file
+ ops, args = getopt.getopt(sys.argv[1:], 's:t:i:d:p:l:h')
+ options = dict( (k,v) for k,v in ops )
+
+ if options.has_key('-h'):
+ usage()
+ if options.has_key('-s'):
+ props.setProperty('constrictor.script', options['-s'])
+ if options.has_key('-t'):
+ props.setProperty('constrictor.numThreads', options['-t'])
+ if options.has_key('-i'):
+ props.setProperty('constrictor.numIterations', options['-i'])
+ if options.has_key('-d'):
+ props.setProperty('constrictor.dbFile', options['-d'])
+ if options.has_key('-p'):
+ props.setProperty('constrictor.port', options['-p'])
+ if options.has_key('-l'):
+ props.setProperty('constrictor.listenAddress', options['-l'])
+
+
+
+def onThreadsComplete(scriptManager):
+ global droneController
+ summary = ScriptThread.currentScriptThread().dbConnection.createTaskSummary()
+ droneController.sendResult(type='task_summary', **summary)
+
+loadProps(PROPS_FILENAME)
+props = Properties.getProperties()
+readArgv()
+initDirs()
+initLog()
+scriptDirs = props.getProperty('constrictor.scriptDirs').split(',')
+
+
+
+if props.getProperty('constrictor.listen') == 'true':
+
+ ''' This is the main controller listen loop. Here, we
+ accept commands from the controller module, perform
+ the action, then go back to listening '''
+
+ droneController = DroneController(
+ props.getProperty('constrictor.address'),
+ int(props.getProperty('constrictor.port')))
+
+ ScriptManager.setOnThreadsComplete(onThreadsComplete)
+
+ while True:
+ try:
+ command = droneController.recv()['command']
+
+ if command['action'] == 'setprop':
+ prop = str(command['prop'])
+ val = str(command['val'])
+ logInfo('setting property %s %s' % (prop, val))
+ props.setProperty(prop, val)
+ continue
+
+ if command['action'] == 'saveprops':
+ logInfo("saving properties back to file")
+ saveProps()
+ continue
+
+ if command['action'] == 'run':
+ ScriptThread.resetThreadSeed()
+ script = props.getProperty('constrictor.script')
+ logInfo('running ' + script)
+ f = openScript(scriptDirs, script)
+ if f:
+ initDB()
+ try:
+ exec(f)
+ except Exception, e:
+ logError("script execution failed: %s" % str(e))
+ droneController.sendError(text=str(e))
+ f.close()
+ continue
+
+ except KeyboardInterrupt:
+ droneController.shutdown()
+
+else:
+ initDB()
+ script = props.getProperty('constrictor.script') # execute the requested script
+ ScriptThread.resetThreadSeed()
+ f = openScript(scriptDirs, script)
+ if f:
+ exec(f)
+ f.close()
+
+
--- /dev/null
+#!/usr/bin/python
+# -----------------------------------------------------------------------
+# Copyright (C) 2007-2008 King County Library System
+# Bill Erickson <erickson@esilibrary.com>
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU 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 General Public License for more details.
+# -----------------------------------------------------------------------
+
+
--- /dev/null
+#!/usr/bin/python
+# -----------------------------------------------------------------------
+# Copyright (C) 2007-2008 King County Library System
+# Bill Erickson <erickson@esilibrary.com>
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU 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 General Public License for more details.
+# -----------------------------------------------------------------------
+
+
+import sys, socket, select, errno, re
+from xml.sax import handler, make_parser, saxutils
+from properties import Properties
+from log import *
+from threading import Thread
+
+SOCKET_TIMEOUT = 4
+READSIZE = 256
+START_STREAM = "<controlset xmlns='http://esilibrary.com/spec/constrictor/v1'>"
+END_STREAM = "</controlset>"
+
+def connected(sock):
+ try:
+ p = sock.getpeername()
+ return True
+ except:
+ return False
+
+class Controller(object):
+ ''' Generic class for sending and receiving controller communications '''
+
+ def __init__(self):
+ self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+ self.queue = []
+ self.remote = None
+ self.parser = None
+ self.connected = False
+ self.streamSent = False
+ self.reset()
+
+ def disconnect(self):
+ ''' Shuts down the socket '''
+ try:
+ self.socket.shutdown(socket.SHUT_RDWR)
+ self.socket.close()
+ except:
+ pass
+
+ def shutdown(self):
+ ''' Shuts down the socket and ends the process '''
+ self.disconnect()
+ sys.exit(0)
+
+ def startStream(self):
+ ''' Sends the openening XML stream tag to initialize the converstaion. '''
+ if not self.streamSent:
+ self.remote.send(START_STREAM)
+ self.streamSent = True
+
+ def endStream(self):
+ ''' Closes the stream document by sending the final closing tag '''
+ self.remote.send(END_STREAM)
+
+ def reset(self):
+ ''' Creates a new stream parser and flags this object as not being
+ connected to the remote node '''
+ logInfo("Resetting controller connection")
+ self.parser = make_parser()
+ self.parser.setContentHandler(CommandParser(self))
+ self.connected = False
+ self.streamSent = False
+
+ def _send(self, tag, **kwargs):
+ ''' Sends an XML tag to the remote node. kwargs are encoded as XML attributes '''
+ s = '<%s' % tag
+ for k,v in kwargs.items():
+ v = re.compile("'").sub('"', str(v)) # change ' to " to prevent xml attr breaking..
+ s += " %s='%s'" % (saxutils.escape(k), saxutils.escape(v))
+ s += '/>'
+ logDebug('Sending ' + s)
+ self.remote.send(s)
+
+
+ def _recv(self):
+ ''' Reads data from the socket and passes it to the SAX parser.
+ This method will continue to read data until at least
+ one event has occrred in the parser (i.e. we have something
+ sitting in the receive queue) '''
+
+ while len(self.queue) == 0:
+
+ if self.remote is None:
+ # we're acting as a server process, waiting for a connection
+ self.remote = self.socket.accept()
+ logInfo('Controller connection from %s' % str(self.remote.remoteAddress))
+
+ try:
+ data = self.remote.recv()
+
+ if data is None:
+ # client severed the connection
+ self.reset()
+ continue
+
+ self.parser.feed(data)
+
+ except KeyboardInterrupt:
+ self.shutdown()
+
+ except socket.error, e:
+ # controller disconnected while we were waiting for a command
+ # go back to the top and wait for a new controller connection
+ if e.args[0] == errno.ECONNRESET:
+ self.reset()
+ continue
+
+ return self.queue.pop(0)
+
+
+ def recv(self):
+ ''' Reads data from the socket and passes it to the SAX parser.
+ This method will continue to read data until at least
+ one event has occrred in the parser (i.e. we have something
+ sitting in the receive queue) '''
+
+ while len(self.queue) == 0:
+
+ if self.remote is None:
+ # we're acting as a server process, waiting for a connection
+ self.remote, address = self.socket.accept()
+ logInfo('Controller connection from %s' % str(address))
+
+ try:
+ data = self.remote.recv(READSIZE)
+
+ # client severed the connection
+ if data is None or data == '':
+ self.reset()
+ continue
+
+ logDebug("Read %s" % data)
+ self.parser.feed(data)
+ self.remote.setblocking(0)
+
+ except KeyboardInterrupt:
+ self.shutdown()
+
+ except socket.error, e:
+
+ # controller disconnected while we were waiting for a command
+ # go back to the top and wait for a new controller connection
+ if e.args[0] == errno.ECONNRESET:
+ self.reset()
+ continue
+
+ # read 0 bytes on a non-blocking socket. set it back
+ # to blocking and see if we're done
+ if e.args[0] == errno.EAGAIN:
+ self.remote.setblocking(1)
+ continue
+
+ return self.queue.pop(0)
+
+
+class DroneController(Controller):
+ ''' Used by a worker drone to receive commands from the console/controller process '''
+
+ def __init__(self, listenAddr='', port=21800):
+ Controller.__init__(self)
+ self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
+ self.socket.bind((listenAddr, port))
+ self.socket.listen(1)
+
+ def sendResult(self, **kwargs):
+ ''' Sends a <result> element '''
+ self._send('result', **kwargs)
+
+ def sendError(self, **kwargs):
+ ''' Sends an <error> element '''
+ self._send('error', **kwargs)
+
+ def reset(self):
+ ''' Resets this drone's connection '''
+ Controller.reset(self)
+ self.remote = None
+
+class GUIController(Controller):
+ ''' Models a single connection from the console GUI to a single drone machine '''
+
+ def __init__(self, remoteAddr, port, id, controlSet):
+ Controller.__init__(self)
+ self.address = remoteAddr
+ self.port = port
+ self.remote = self.socket
+ self.socket.settimeout(SOCKET_TIMEOUT)
+ self.id = id
+ self.name = '%s:%s' % (remoteAddr, port)
+ self.failed = None
+ self.connect()
+
+ def connect(self):
+ try:
+ self.socket.connect((self.address, self.port))
+ self.reset()
+ self.startStream()
+ while not self.connected:
+ self.parser.feed(self.remote.recv(READSIZE))
+
+ except socket.error, e:
+ err = _("Error connecting to %s:%s %s" % (self.address, self.port, str(e)))
+ self.failed = err
+ logError(err)
+ return False
+
+ except socket.timeout:
+ err = _("Connection to %s:%s timed out" % (self.address, self.port))
+ self.failed = err
+ logError(err)
+ return False
+
+ def __str__(self):
+ return self.name
+
+ def sendCommand(self, **kwargs):
+ self._send('command', **kwargs)
+
+
+class GUIControllerSet(object):
+ def __init__(self):
+ self.controllers = [] # our list of connected controllers
+ self.testBatches = []
+ self.connectedHandler = None
+ self.failedControllers = []
+
+ def createController(self, remoteAddr, port, id):
+ c = GUIController(remoteAddr, port, id, self)
+ if c.connected:
+ self.controllers.append(c)
+ else:
+ self.failedControllers.append(c)
+
+ def isEmpty(self):
+ if len(self.controllers) == 0:
+ return True
+ return False
+
+ def checkReady(self, timeout):
+ ''' Returns a list of controller objects
+ that have data available for recv()'ing '''
+ rlist = [ c.socket for c in self.controllers if connected(c.socket) ]
+ if len(rlist) > 0:
+ try:
+ rlist = select.select(rlist, [], [], timeout)
+ except Exception, e:
+ logError("select() failed with " + str(e))
+ return []
+ return [ c for c in self.controllers if c.socket in rlist[0] ]
+ return []
+
+ def pushReady(self, controllers):
+ self.responded += controllers
+
+ def broadcastCommand(self, **kwargs):
+ for c in self.controllers:
+ try:
+ c.sendCommand(**kwargs)
+ except socket.error, e:
+ self.controllers.remove(c)
+ except socket.timeout, t:
+ self.controllers.remove(c)
+
+ def sendCommand(self, controllerID, **kwargs):
+ c = [ co for co in self.controllers if int(co.id) == int(controllerID) ]
+ if len(c) == 0: return
+ c[0].sendCommand(**kwargs);
+
+ def getSet(self):
+ return self.controllers
+
+ def getFailed(self):
+ return self.failedControllers
+
+ def reset(self):
+ while len(self.controllers) > 0:
+ self.controllers.pop().disconnect()
+
+ def runBatch(self):
+ b = GUIControllerSet.TestBatch(self)
+ self.broadcastCommand(action='run')
+ self.testBatches.append(b)
+ return b
+
+ def getLastBatch(self):
+ return self.testBatches[len(self.testBatches)]
+
+ class TestBatch(object):
+ def __init__(self, controlSet):
+ self.responded = {}
+ self.controlSet = controlSet
+ self.complete = False
+
+ def recv(self, timeout):
+ ready = self.controlSet.checkReady(timeout)
+ for c in ready:
+ self.responded[c.name] = c.recv()['result']
+ if len(self.responded) == len(self.controlSet.getSet()):
+ self.complete = True
+ return self.responded
+
+ def getResponded(self):
+ ''' Returns a list of controllers that have responded from this batch '''
+ return self.responded
+
+ def clearResponded(self):
+ self.responded = {}
+
+
+
+
+class CommandParser(handler.ContentHandler):
+
+ def __init__(self, controller):
+ self.controller = controller
+
+ def getAttr(self, attrs, name):
+ for (k, v) in attrs.items():
+ if k == name:
+ return v
+
+ def __xmlNode2Python(self, name, attrs):
+ p = {}
+ p[name] = dict([(k,v) for k,v in attrs.items()])
+ return p
+
+ def startElement(self, name, attrs):
+ logDebug("Received %s %s" % (name, str([(k,v) for k,v in attrs.items()])))
+
+ if name == 'controlset':
+ if not self.controller.connected:
+ self.controller.connected = True
+ self.controller.startStream()
+ return
+
+ p = self.__xmlNode2Python(name, attrs)
+
+ try:
+ if p['command']['action'] == 'shutdown':
+ self.controller.shutdown()
+ except KeyError: pass
+
+ self.controller.queue.append(p)
+
--- /dev/null
+# -----------------------------------------------------------------------
+# Copyright (C) 2007-2008 King County Library System
+# Bill Erickson <erickson@esilibrary.com>
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU 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 General Public License for more details.
+# -----------------------------------------------------------------------
+
+from pysqlite2 import dbapi2 as sqlite
+import sys, threading, thread, time
+from log import *
+from properties import Properties
+
+# wrap DB access in a semaphore
+dbSema = threading.BoundedSemaphore(value=1)
+
+
+class DBConnection(object):
+
+ # the ID of the currently executing task set
+ taskSetID = 0
+
+ def __init__(self, dbFile):
+ self.conn = sqlite.connect(dbFile)
+
+ def createTaskSet(self):
+ SQL = '''
+ insert into task_set (start_time) values (%f)
+ ''' % time.time()
+ cur = self.execute(SQL, True)
+ DBConnection.taskSetID = cur.lastrowid
+
+ def finishTaskSet(self):
+ SQL = '''
+ update task_set set end_time = %f where id = %d
+ ''' % (time.time(), DBConnection.taskSetID)
+ self.execute(SQL, True)
+
+
+ def createTables(self):
+ taskSQL = '''
+ CREATE TABLE IF NOT EXISTS task (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ task_name TEXT,
+ task_set INTEGER,
+ runtime REAL,
+ runner INTEGER,
+ thread_id INTEGER,
+ duration INTEGER,
+ success INTEGER
+ );
+ '''
+
+ taskSetSQL = '''
+ CREATE TABLE IF NOT EXISTS task_set (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ start_time REAL,
+ end_time REAL
+ );
+ '''
+
+ self.execute(taskSQL)
+ self.execute(taskSetSQL, True)
+
+
+ def createTaskSummary(self):
+ summary = {}
+
+ cur = self.execute(
+ 'select end_time - start_time from task_set where id = %d' % DBConnection.taskSetID)
+ row = cur.fetchone()
+ summary['task_set_duration'] = float(row[0]) or ''
+
+ cur = self.execute(
+ 'select avg(duration) from task where success = 1 and task_set = %d' % DBConnection.taskSetID)
+ row = cur.fetchone()
+ summary['avg_task_duration'] = row[0] or ''
+
+ cur = self.execute(
+ 'select count(*) from task where success = 1 and task_set = %d' % DBConnection.taskSetID)
+ row = cur.fetchone()
+ summary['num_task_success'] = row[0]
+
+ cur = self.execute(
+ 'select count(*) from task where success = 0 and task_set = %d' % DBConnection.taskSetID)
+ row = cur.fetchone()
+ summary['num_task_failed'] = row[0]
+
+ summary['amortized_task_duration'] = ''
+ if summary['num_task_success'] > 0:
+ summary['amortized_task_duration'] = \
+ float(summary['task_set_duration']) / float(summary['num_task_success'])
+
+ summary['amortized_tasks_per_second'] = ''
+ if summary['num_task_success'] > 0:
+ summary['amortized_tasks_per_second'] = 1.0 / float(summary['amortized_task_duration'])
+
+ props = Properties.getProperties()
+ summary['thread_count'] = props.getProperty('constrictor.numThreads')
+ summary['iteration_count'] = props.getProperty('constrictor.numIterations')
+
+ self.makeTaskTypeSummary(summary)
+
+ logInfo('created summary %s' % summary)
+ return summary
+
+ def makeTaskTypeSummary(self, summary):
+ ''' build a summary string for each task type of <task_name>:<num_runs>:<avg_duration>; '''
+
+ summary['task_type_summary'] = ''
+ tasks = self.execute(
+ 'select distinct(task_name) from task where task_set = %d' % DBConnection.taskSetID)
+
+ for t in tasks:
+ t = t[0]
+
+ # grab the average duration for this type of task
+ cur = self.execute(
+ 'select avg(duration) from task where task_set = %d and task_name = "%s"' % (
+ DBConnection.taskSetID, t))
+ avg = cur.fetchone()[0]
+
+ # grab the number of times this task was run
+ cur = self.execute(
+ 'select count(*) from task where task_set = %d and task_name = "%s"' % (
+ DBConnection.taskSetID, t))
+ row = cur.fetchone()
+ count = row[0]
+
+ summary['task_type_summary'] += '%s:%d:%f;' % (t, count, float(avg))
+
+
+
+
+ def dropTables(self):
+ SQL = 'drop table task;'
+ self.execute(SQL, True)
+
+
+ def disconnect(self):
+ if self.conn is not None:
+ self.conn.close()
+
+
+ def execute(self, sql, commit=False):
+ logDebug('SQL ' + sql)
+ from script import ScriptThread
+ cur = None
+ try:
+ dbSema.acquire()
+ cur = self.conn.cursor()
+ cur.execute(sql)
+ if commit:
+ self.conn.commit()
+ except Exception, e:
+ sys.stderr.write('DB error: thread = %d : %s\n' % (ScriptThread.getThreadID(), str(e)))
+ sys.stderr.flush()
+ dbSema.release()
+ sys.exit(1)
+
+ dbSema.release()
+ return cur
+
+ def insertTask(self, taskRunner):
+ from script import ScriptThread
+
+ SQL = """
+ insert into task (task_name, runtime, runner, thread_id, task_set, duration, success)
+ values ('%s', %f, %d, %d, %d, %f, %d) """ % (
+ taskRunner.task.name,
+ time.time(),
+ 1, # XXX get me from the task runner ?
+ ScriptThread.getThreadID(),
+ DBConnection.taskSetID,
+ taskRunner.duration,
+ taskRunner.success
+ )
+ self.execute(SQL, True)
+
+
+
--- /dev/null
+#!/usr/bin/python
+# -----------------------------------------------------------------------
+# Copyright (C) 2007-2008 King County Library System
+# Bill Erickson <erickson@esilibrary.com>
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU 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 General Public License for more details.
+# -----------------------------------------------------------------------
+
+
+from properties import Properties
+import sys
+
+loglevel = 1
+def initLog():
+ global loglevel
+ props = Properties.getProperties()
+ loglevel = int(props.getProperty('constrictor.loglevel') or 0)
+
+def logError(msg=''):
+ if loglevel < 1: return
+ sys.stderr.write('Error*: %s\n' % msg)
+ sys.stderr.flush()
+
+def logInfo(msg=''):
+ if loglevel < 2: return
+ print 'Info: %s' % msg
+
+def logDebug(msg=''):
+ if loglevel < 3: return
+ print 'Debug: %s' % msg
--- /dev/null
+"""
+A Python replacement for java.util.Properties class
+This is modelled as closely as possible to the Java original.
+
+Created - Anand B Pillai <abpillai@gmail.com>
+
+Edited by Bill Erickson <billserickson@gmail.com>
+ - added getProperties to return a global properties object
+ - added property name sorting to the store() method
+"""
+
+import sys,os
+import re
+import time
+
+class IllegalArgumentException(Exception):
+
+ def __init__(self, lineno, msg):
+ self.lineno = lineno
+ self.msg = msg
+
+ def __str__(self):
+ s='Exception at line number %d => %s' % (self.lineno, self.msg)
+ return s
+
+globalProps = None
+class Properties(object):
+ """ A Python replacement for java.util.Properties """
+
+ def __init__(self, props=None):
+ # Note: We don't take a default properties object
+ # as argument yet
+
+ # Dictionary of properties.
+ self._props = {}
+ # Dictionary of properties with 'pristine' keys
+ # This is used for dumping the properties to a file
+ # using the 'store' method
+ self._origprops = {}
+
+ # Dictionary mapping keys from property
+ # dictionary to pristine dictionary
+ self._keymap = {}
+
+ self.othercharre = re.compile(r'(?<!\\)(\s*\=)|(?<!\\)(\s*\:)')
+ self.othercharre2 = re.compile(r'(\s*\=)|(\s*\:)')
+ self.bspacere = re.compile(r'\\(?!\s$)')
+
+ def getProperties():
+ global globalProps
+ return globalProps
+ getProperties = staticmethod(getProperties)
+
+ def setGlobalProperties(props):
+ global globalProps
+ globalProps = props
+ setGlobalProperties = staticmethod(setGlobalProperties)
+
+ def __str__(self):
+ s='{'
+ for key,value in self._props.items():
+ s = ''.join((s,key,'=',value,', '))
+
+ s=''.join((s[:-2],'}'))
+ return s
+
+ def __parse(self, lines):
+ """ Parse a list of lines and create
+ an internal property dictionary """
+
+ # Every line in the file must consist of either a comment
+ # or a key-value pair. A key-value pair is a line consisting
+ # of a key which is a combination of non-white space characters
+ # The separator character between key-value pairs is a '=',
+ # ':' or a whitespace character not including the newline.
+ # If the '=' or ':' characters are found, in the line, even
+ # keys containing whitespace chars are allowed.
+
+ # A line with only a key according to the rules above is also
+ # fine. In such case, the value is considered as the empty string.
+ # In order to include characters '=' or ':' in a key or value,
+ # they have to be properly escaped using the backslash character.
+
+ # Some examples of valid key-value pairs:
+ #
+ # key value
+ # key=value
+ # key:value
+ # key value1,value2,value3
+ # key value1,value2,value3 \
+ # value4, value5
+ # key
+ # This key= this value
+ # key = value1 value2 value3
+
+ # Any line that starts with a '#' is considerered a comment
+ # and skipped. Also any trailing or preceding whitespaces
+ # are removed from the key/value.
+
+ # This is a line parser. It parses the
+ # contents like by line.
+
+ lineno=0
+ i = iter(lines)
+
+ for line in i:
+ lineno += 1
+ line = line.strip()
+ # Skip null lines
+ if not line: continue
+ # Skip lines which are comments
+ if line[0] == '#': continue
+ # Some flags
+ escaped=False
+ # Position of first separation char
+ sepidx = -1
+ # A flag for performing wspace re check
+ flag = 0
+ # Check for valid space separation
+ # First obtain the max index to which we
+ # can search.
+ m = self.othercharre.search(line)
+ if m:
+ first, last = m.span()
+ start, end = 0, first
+ flag = 1
+ wspacere = re.compile(r'(?<![\\\=\:])(\s)')
+ else:
+ if self.othercharre2.search(line):
+ # Check if either '=' or ':' is present
+ # in the line. If they are then it means
+ # they are preceded by a backslash.
+
+ # This means, we need to modify the
+ # wspacere a bit, not to look for
+ # : or = characters.
+ wspacere = re.compile(r'(?<![\\])(\s)')
+ start, end = 0, len(line)
+
+ m2 = wspacere.search(line, start, end)
+ if m2:
+ # print 'Space match=>',line
+ # Means we need to split by space.
+ first, last = m2.span()
+ sepidx = first
+ elif m:
+ # print 'Other match=>',line
+ # No matching wspace char found, need
+ # to split by either '=' or ':'
+ first, last = m.span()
+ sepidx = last - 1
+ # print line[sepidx]
+
+
+ # If the last character is a backslash
+ # it has to be preceded by a space in which
+ # case the next line is read as part of the
+ # same property
+ while line[-1] == '\\':
+ # Read next line
+ nextline = i.next()
+ nextline = nextline.strip()
+ lineno += 1
+ # This line will become part of the value
+ line = line[:-1] + nextline
+
+ # Now split to key,value according to separation char
+ if sepidx != -1:
+ key, value = line[:sepidx], line[sepidx+1:]
+ else:
+ key,value = line,''
+
+ self.processPair(key, value)
+
+ def processPair(self, key, value):
+ """ Process a (key, value) pair """
+
+ oldkey = key
+ oldvalue = value
+
+ # Create key intelligently
+ keyparts = self.bspacere.split(key)
+ # print keyparts
+
+ strippable = False
+ lastpart = keyparts[-1]
+
+ if lastpart.find('\\ ') != -1:
+ keyparts[-1] = lastpart.replace('\\','')
+
+ # If no backspace is found at the end, but empty
+ # space is found, strip it
+ elif lastpart and lastpart[-1] == ' ':
+ strippable = True
+
+ key = ''.join(keyparts)
+ if strippable:
+ key = key.strip()
+ oldkey = oldkey.strip()
+
+ oldvalue = self.unescape(oldvalue)
+ value = self.unescape(value)
+
+ self._props[key] = value.strip()
+
+ # Check if an entry exists in pristine keys
+ if self._keymap.has_key(key):
+ oldkey = self._keymap.get(key)
+ self._origprops[oldkey] = oldvalue.strip()
+ else:
+ self._origprops[oldkey] = oldvalue.strip()
+ # Store entry in keymap
+ self._keymap[key] = oldkey
+
+ def escape(self, value):
+
+ # Java escapes the '=' and ':' in the value
+ # string with backslashes in the store method.
+ # So let us do the same.
+ newvalue = value.replace(':','\:')
+ newvalue = newvalue.replace('=','\=')
+
+ return newvalue
+
+ def unescape(self, value):
+
+ # Reverse of escape
+ newvalue = value.replace('\:',':')
+ newvalue = newvalue.replace('\=','=')
+
+ return newvalue
+
+ def load(self, stream):
+ """ Load properties from an open file stream """
+
+ # For the time being only accept file input streams
+ if type(stream) is not file:
+ raise TypeError,'Argument should be a file object!'
+ # Check for the opened mode
+ if stream.mode != 'r':
+ raise ValueError,'Stream should be opened in read-only mode!'
+
+ try:
+ lines = stream.readlines()
+ self.__parse(lines)
+ except IOError, e:
+ raise
+
+ def getProperty(self, key):
+ """ Return a property for the given key """
+
+ return self._props.get(key,'')
+
+ def setProperty(self, key, value):
+ """ Set the property for the given key """
+
+ if type(key) is str and type(value) is str:
+ self.processPair(key, value)
+ else:
+ raise TypeError,'both key and value should be strings!'
+
+ def propertyNames(self):
+ """ Return an iterator over all the keys of the property
+ dictionary, i.e the names of the properties """
+
+ return self._props.keys()
+
+ def list(self, out=sys.stdout):
+ """ Prints a listing of the properties to the
+ stream 'out' which defaults to the standard output """
+
+ out.write('-- listing properties --\n')
+ for key,value in self._props.items():
+ out.write(''.join((key,'=',value,'\n')))
+
+ def store(self, out, header=""):
+ """ Write the properties list to the stream 'out' along
+ with the optional 'header' """
+
+ if out.mode[0] != 'w':
+ raise ValueError,'Steam should be opened in write mode!'
+
+ try:
+ out.write(''.join(('#',header,'\n')))
+ # Write timestamp
+ tstamp = time.strftime('%a %b %d %H:%M:%S %Z %Y', time.localtime())
+ out.write(''.join(('#',tstamp,'\n')))
+ # Write properties from the pristine dictionary
+ props = self._origprops
+ keys = props.keys()
+ keys.sort()
+ for k in keys:
+ out.write(''.join((k,'=',self.escape(props[k]),'\n')))
+
+ out.close()
+ except IOError, e:
+ raise
+
+ def getPropertyDict(self):
+ return self._props
+
+ def __getitem__(self, name):
+ """ To support direct dictionary like access """
+
+ return self.getProperty(name)
+
+ def __setitem__(self, name, value):
+ """ To support direct dictionary like access """
+
+ self.setProperty(name, value)
+
+ def __getattr__(self, name):
+ """ For attributes not found in self, redirect
+ to the properties dictionary """
+
+ try:
+ return self.__dict__[name]
+ except KeyError:
+ if hasattr(self._props,name):
+ return getattr(self._props, name)
+
+if __name__=="__main__":
+ p = Properties()
+ p.load(open('test2.properties'))
+ p.list()
+ print p
+ print p.items()
+ print p['name3']
+ p['name3'] = 'changed = value'
+ print p['name3']
+ p['new key'] = 'new value'
+ p.store(open('test2.properties','w'))
--- /dev/null
+#!/usr/bin/python
+# -----------------------------------------------------------------------
+# Copyright (C) 2007-2008 King County Library System
+# Bill Erickson <erickson@esilibrary.com>
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU 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 General Public License for more details.
+# -----------------------------------------------------------------------
+
+import time, sys, threading
+from threading import Thread, local
+from properties import Properties
+from db import DBConnection
+import threading
+from log import *
+
+
+class Script(object):
+ def __init__(self, name=''):
+ self.name = name
+ def run(self):
+ logError('Override Script.run() to run a script!')
+ sys.exit(1)
+ def onThreadInit(self, scriptThread):
+ pass
+ def onThreadComplete(self, scriptThread):
+ pass
+
+
+class ScriptThread(Thread):
+ """ Models a single thread of execution for a script.
+ This class will run the script object up to numItr iterations.
+ """
+ # this is used to determine the thread ID of newly created threads
+ threadSeed = 0
+
+ def __init__(self, script, numItr, onComplete=None ):
+ Thread.__init__(self)
+ logDebug("Creating thread with ID %d" % ScriptThread.threadSeed)
+ self.script = script # our script object
+ self.numItr = numItr # number of times to run our script
+ self.setName(ScriptThread.threadSeed) # use the Thread name as our threadID
+ self.dbConnection = None # create this after the thread has launched
+ self.onComplete = onComplete
+ ScriptThread.threadSeed += 1
+ self.userData = None
+
+ def resetThreadSeed():
+ ScriptThread.threadSeed = 0
+ resetThreadSeed = staticmethod(resetThreadSeed)
+
+ def run(self):
+ """ Run our script object up to numItr times."""
+ self.initThread()
+ tid = ScriptThread.getThreadID()
+ self.script.onThreadInit(self)
+ for i in range(0, self.numItr):
+ logInfo('running thread %d, iteration %d' % (tid, i))
+ try:
+ self.script.run()
+ except Exception, e:
+ logError("Script exception: %s" % str(e))
+ break
+
+ self.script.onThreadComplete(self)
+
+ if self.onComplete:
+ self.onComplete(self)
+
+
+ def initThread(self):
+ """ Perform any thread-specific house keeping."""
+ data = ScriptThread.__threadData()
+ data.scriptThread = self
+
+ props = Properties.getProperties()
+ self.dbConnection = DBConnection(props.getProperty('constrictor.dbFile'))
+
+
+ def getThreadID():
+ """ Returns the ID of the current thread.
+ Returns -1 if this is the main thread
+ """
+ i = -1
+ try:
+ i = int(threading.currentThread().getName())
+ except:
+ pass
+ return i
+ getThreadID = staticmethod(getThreadID)
+
+
+ def currentScriptThread():
+ data = ScriptThread.__threadData()
+ return data.scriptThread
+ currentScriptThread = staticmethod(currentScriptThread)
+
+
+ threadDataStore = None
+ def __threadData():
+ if ScriptThread.threadDataStore is None:
+ ScriptThread.threadDataStore = threading.local()
+ return ScriptThread.threadDataStore
+ __threadData = staticmethod(__threadData)
+
+
+class ScriptManager(object):
+ """ Manages the script threads. """
+
+ # called when all script threads have completed
+ onThreadsComplete = None
+
+ def __init__(self, script, numThreads, numItr):
+ """ numThreads - number of simultaneous script threads to run
+ numItr - number of times to repeat a script within a given thread
+ """
+ self.script = script
+ self.numThreads = numThreads
+ self.numItr = numItr
+ self.numComplete = 0
+
+ if self.script is None:
+ logError('Please register a script to run')
+ sys.exit(1)
+
+
+ def setOnThreadsComplete(func):
+ ScriptManager.onThreadsComplete = staticmethod(func)
+ setOnThreadsComplete = staticmethod(setOnThreadsComplete)
+
+ def runScriptThreads(self):
+
+ def onComplete(scriptThread):
+ self.numComplete += 1
+ if self.numComplete == self.numThreads:
+ # all threads have completed
+ scriptThread.dbConnection.finishTaskSet()
+ logInfo('all threads done.. finishing task set')
+ if ScriptManager.onThreadsComplete:
+ ScriptManager.onThreadsComplete(ScriptManager)
+
+ """ Launches the script threads. """
+ for i in range(0, self.numThreads):
+ tt = ScriptThread(self.script, self.numItr, onComplete)
+ tt.start()
+
+ def go(script):
+ """ The main script passes control to the ScriptManager via this function.
+ Here, we parse the config and launch the script threads
+ """
+ props = Properties.getProperties()
+
+ threads = int(props.getProperty('constrictor.numThreads'))
+ itrs = int(props.getProperty('constrictor.numIterations'))
+
+ logDebug('launching %d threads with %d iterations' % (threads, itrs))
+
+ manager = ScriptManager(script, threads, itrs)
+ manager.runScriptThreads()
+
+ go = staticmethod(go)
+
+
+
+
--- /dev/null
+#!/usr/bin/python
+# -----------------------------------------------------------------------
+# Copyright (C) 2007-2008 King County Library System
+# Bill Erickson <erickson@esilibrary.com>
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU 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 General Public License for more details.
+# -----------------------------------------------------------------------
+
+
+import time, sys
+from script import ScriptThread
+from threading import local
+import threading
+from log import *
+
+
+class Task(object):
+ """ A Task represents a single unit of work. They are created
+ before thread allocation, so no state information is stored
+ on the actual Task object.
+ """
+
+ def __init__(self, name=''):
+ self.name = name
+ def run(self, **kwargs):
+ """Override this method with the work to perform"""
+ logError('Override me!')
+ def wrap(self):
+ """ Static method for wrapping and registering a task.
+ Statistics will only be collected for registered tasks.
+ """
+ return TaskWrapper(self)
+
+
+class TaskWrapper(object):
+ """ Provides a new run() method for the client to call directly.
+ The main purpose of the TaskWrapper is to allow the creation of
+ a TaskRunner after thread creation.
+ """
+
+ def __init__(self, task):
+ self.task = task
+ def run(self, **kwargs):
+ return TaskRunner(self.task).go(**kwargs)
+
+
+
+class TaskRunner(object):
+ """ Runs a single task and collects timing information on the task for a given thread. """
+
+ def __init__(self, task):
+ self.task = task
+ self.duration = 0
+ self.success = False
+ self.complete = False
+
+ def go(self, **kwargs):
+ start = time.time()
+ ret = None # capture the return value from the task
+
+ try:
+ ret = self.task.run(**kwargs)
+ self.duration = time.time() - start
+ self.complete = True
+ self.success = True
+ except Exception, E:
+ sys.stderr.write(str(E))
+ # store the error info somewhere?
+
+ logDebug('%s: thread = %d : duration = %f' % (
+ self.task.name, ScriptThread.getThreadID(), self.duration))
+
+ sys.stdout.flush()
+ dbConn = ScriptThread.currentScriptThread().dbConnection
+ dbConn.insertTask(self)
+ return ret
+
+
+
+
--- /dev/null
+#!/usr/bin/python
+# -----------------------------------------------------------------------
+# Copyright (C) 2007-2008 King County Library System
+# Bill Erickson <erickson@esilibrary.com>
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU 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 General Public License for more details.
+# -----------------------------------------------------------------------
+
+import sys, getopt, os, errno
+from constrictor.properties import Properties
+from constrictor.db import DBConnection
+from constrictor.log import *
+
+DEFAULT_PORT = 21800
+CONFIG_FILENAME = 'config.xml'
+PROPS_FILENAME = 'constrictor.properties'
+
+props = Properties()
+
+def loadProps(file):
+
+ try:
+ # parse the properties file
+ p = open(PROPS_FILENAME)
+ Properties.setGlobalProperties(props)
+ props.load(p)
+ p.close()
+ except IOError:
+ print '''
+ WARNING: No properties file (constrictor.properties) found.
+ Using command line options only.
+ '''
+
+def saveProps():
+ try:
+ p = open(PROPS_FILENAME, 'w')
+ props.store(p)
+ p.close()
+ except Exception, e:
+ print "WARNING: Unable to store properties to file\n%s" % str(e)
+
+
+def initDB():
+ ''' connect to the db and make sure the tables exist '''
+ dbConnection = DBConnection(props.getProperty('constrictor.dbFile'))
+ dbConnection.createTables()
+ dbConnection.createTaskSet()
+ dbConnection.disconnect()
+
+
+def initDirs():
+ cachedir = props.getProperty('constrictor.cacheDir')
+ if not os.path.exists(cachedir):
+ os.mkdir(props.getProperty('constrictor.cacheDir'))
+
+pathsAdded = []
+def openScript(dirs, script):
+ ''' Finds the script file in the set of script diretories,
+ opens the file and returns the file object '''
+ f = None
+ for d in dirs:
+ try:
+ # try to open the script file
+ f = open(os.path.join(d, script))
+
+ # add the script directory to the python path
+ if d not in pathsAdded:
+ sys.path.append(os.path.join(os.path.abspath(''), d))
+ pathsAdded.append(d)
+
+ return f
+ except IOError:
+ pass
+ logError("Unable to find script %s in path %s" % (script, str(dirs)))
+ return None
+
+
+
+
+
--- /dev/null
+# -----------------------------------------------------------------------
+# Copyright (C) 2007-2008 King County Library System
+# Bill Erickson <erickson@esilibrary.com>
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU 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 General Public License for more details.
+# -----------------------------------------------------------------------
+
+from django.db import models
+
+
+class Drone(models.Model):
+ address = models.CharField(maxlength=200)
+ port = models.IntegerField()
+ enabled = models.BooleanField(blank=True)
+ class Admin:
+ list_display = ('address', 'port', 'enabled')
+ def __str__(self):
+ return "%s:%d" % (self.address, self.port)
+
+class Plugin(models.Model):
+ name = models.CharField(maxlength=100)
+ description = models.TextField(maxlength=500)
+ class Admin:
+ pass
+ def __str__(self):
+ return self.name
+
+class Script(models.Model):
+ plugin = models.ForeignKey(Plugin)
+ name = models.CharField(maxlength=200)
+ description = models.TextField(maxlength=500)
+ class Admin:
+ list_display = ('plugin', 'name', 'description')
+ def __str__(self):
+ return self.name
+
+class Property(models.Model):
+ plugin = models.ForeignKey(Plugin)
+ name = models.CharField(maxlength=100)
+ description = models.TextField(maxlength=500)
+ class Admin:
+ list_display = ('plugin', 'name', 'description')
+ def __str__(self):
+ return self.name
--- /dev/null
+{% extends "admin/base.html" %}
+{% load i18n %}
+
+{% block title %}{{ title|escape }}{% endblock %}
+
+{% block branding %}
+<h1 id="site-name">{% trans 'Constrictor Dashboard' %}</h1>
+{% endblock %}
+
+{% block nav-global %}
+<!-- expand the main content div so we have some more room -->
+<style type='text/css'>#content-main { width: 570px; }</style>
+<a style='padding-left: 10px;' href='/control/'>{% trans 'Constrictor Home' %}</a>
+<a style='padding-left: 10px;' href='/admin/logout'>{% trans 'Logout' %}</a>
+{% endblock %}
--- /dev/null
+<!-- =============================================================
+ Copyright (C) 2007 King County Library System
+
+ This program is free software; you can redistribute it and/or
+ modify it under the terms of the GNU 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 General Public License for more details.
+ ================================================================= -->
+
+<!-- Navigation / Actions area -->
+<div class='module'>
+ <script>
+ function submitFormValue(url, id, isSelect) {
+ param = ''
+ if(isSelect) {
+ selector = document.getElementById(id);
+ param = selector[selector.selectedIndex].value
+ } else {
+ param = document.getElementById(id).value
+ }
+ location.href = url + param
+ }
+ </script>
+ <style>
+ #actions_table td { vertical-align: middle; }
+ </style>
+ <h2>Configure and Run Tests</h2>
+ <table width='100%'>
+ <tbody>
+ <tr class='row1'>
+ <td>
+ <form action='/control/updateBasicProps' method='get'>
+ <table id='actions_table'><tr>
+ <td>Thread Count
+ <input name='threadCount' type='text' size='2' value='{{ globalThreadCount }}'/></td>
+ <td>Iteration Count
+ <input name='iterationCount' type='text' size='2' value='{{ globalIterationCount }}'/></td>
+ <td>Test Script</td>
+ <td>
+ <select name='selectedScript'>
+ {% for s in scripts %}
+ <option value='{{ s.name }}'
+ {% ifequal s.name selectedScript %}selected='selected'{%endifequal%}>
+ {{ s.name }}</option>
+ {% endfor %}
+ </select>
+ </td>
+ <td><input type='submit' value='Apply'/></td>
+ </tr></table>
+ </form>
+ </td>
+ </tr>
+
+ <!--
+ <tr class='row1'>
+ <th>
+ <a href='javascript:submitFormValue("/control/updateThreads/?count=", "thread_count");'
+ >Set Thread Count</a>
+ </th>
+ <td><input id='thread_count' type='text' size='2' value='{{ globalThreadCount }}'/></td>
+ </tr>
+ <tr class='row2'>
+ <th>
+ <a href='javascript:submitFormValue("/control/updateIterations/?count=", "iteration_count")'
+ >Set Iterations-Per-Thread</a>
+ </th>
+ <th><input id='iteration_count' type='text' size='2' value='{{ globalIterationCount }}'/></th>
+ </tr>
+ <tr class='row1'>
+ <th>
+ <a href='javascript:submitFormValue("/control/setScript/?script=", "script_name", true)'
+ >Set Test Script</a>
+ </th>
+ <th>
+ <select id='script_name'>
+ {% for s in scripts %}
+ <option value='{{ s.name }}'
+ {% ifequal s.name selectedScript %}selected='selected'{%endifequal%}>
+ {{ s.name }}</option>
+ {% endfor %}
+ </select>
+ </th>
+ </tr>
+ -->
+
+ <tr class='row2'>
+ <th style='text-align: center;'>
+ <a href='/control/reset/'>Reset Connections</a>
+ ·
+ <a href='/control/run/'>Run Tests</a>
+ </th>
+ </tr>
+
+ </tbody>
+ </table>
+</div>
+
--- /dev/null
+<!-- =============================================================
+ Copyright (C) 2007 King County Library System
+
+ This program is free software; you can redistribute it and/or
+ modify it under the terms of the GNU 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 General Public License for more details.
+ ================================================================= -->
+<div class='module'>
+ <h2>Configure Scripts and Drones</h2>
+ <table>
+ <tbody>
+ <tr class='row1'>
+ <th colspan='2'><a href='/admin/control/drone/'>Edit Connected Drones</a></th>
+ </tr>
+ <tr class='row2'>
+ <th colspan='2'><a href='/admin/control/script/'>Edit Test Scripts</a></th>
+ </tr>
+ <tr class='row1'>
+ <th colspan='2'><a href='/control/setProps/'>Set Test Properties</a></th>
+ </tr>
+ </tbody>
+ </table>
+</div>
+
--- /dev/null
+<!-- =============================================================
+ Copyright (C) 2007 King County Library System
+
+ This program is free software; you can redistribute it and/or
+ modify it under the terms of the GNU 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 General Public License for more details.
+ ================================================================= -->
+
+{% extends 'admin/base_site.html' %}
+{% load i18n %}
+{% block coltype %}colMS{% endblock %}
+{% block bodyclass %}dashboard{% endblock %}
+{% block breadcrumbs %}{% endblock %}
+
+{% block content %}
+ <a href='/control/'>Home</a>
+ <br/><br/>
+ <div class='module'>
+ <h2>Glossary</h2>
+ <table><tbody>
+ <tr class='row1'>
+ <th>Task</th>
+ <td>
+ A single, timed unit of work. A task usually corresponds to an atomic action.
+ </td>
+ </tr>
+ <tr class='row2'>
+ <th>Script</th>
+ <td>
+ A collection of one or more tasks performed in a certain sequence.
+ </td>
+ </tr>
+ </tbody></table>
+ </div>
+ <br/>
+ <div class='module'>
+ <h2>Configure and Run Tests</h2>
+ <table>
+ <tbody>
+ <tr class='row1'>
+ <th>Thread Count</th>
+ <td>
+ The number of simultaneous threads of execution on each of the
+ connected drones.
+ </td>
+ </tr>
+ <tr class='row2'>
+ <th>Iterations-per-Thread</th>
+ <td>
+ The number of times a test will be run within each thread of
+ execution on each drone. The total number of tests run on a given drone
+ will be the number of threads times the number of iterations per thread.
+ </td>
+ </tr>
+ <tr class='row1'>
+ <th>Test Script</th>
+ <td>
+ The test script to run the next time tests are run.
+ </td>
+ </tr>
+ <tr class='row2'>
+ <th>Reset Connections</th>
+ <td>
+ disconnects and attempts to reconnect to all configured drones.
+ Use this after adding or removing drones from the configuration
+ or after any of the services on the connected drones has been restarted.
+ </td>
+ </tr>
+ <tr class='row1'>
+ <th>Run Tests</th>
+ <td>Tells all connected drones to run the currently configured test</td>
+ </tr>
+ </tbody>
+ </table>
+ </div>
+ <br/>
+ <div class='module'>
+ <h2>Configure Scripts and Drones</h2>
+ <table>
+ <tbody>
+ <tr class='row1'>
+ <th>Edit Connected Drones</th>
+ <td>
+ This option allows you to add, edit, and remove connected test drones.
+ </td>
+ </tr>
+ <tr class='row2'>
+ <th>Edit Test Scripts</th>
+ <td>
+ This option allows you to see and edit information about configured test scripts.
+ </td>
+ </tr>
+ <tr class='row1'>
+ <th>Set Test Properties</th>
+ <td>
+ This option allows you to set test-specific properties</td>
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ </div>
+ <br/>
+ <div class='module'>
+ <h2>Drone Status</h2>
+ <div>
+ Displays the list of configured drones and the current status of the
+ connections to those drones.
+ </div>
+ </div>
+{% endblock %}
+
--- /dev/null
+<!-- =============================================================
+ Copyright (C) 2007 King County Library System
+
+ This program is free software; you can redistribute it and/or
+ modify it under the terms of the GNU 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 General Public License for more details.
+ ================================================================= -->
+
+{% load i18n %}
+<div class='module'>
+ <h2>Drone Status</h2>
+ <table>
+ <tbody>
+ {% for data in droneData %}
+ <tr>
+ <th>
+ <a href='/admin/control/drone/{{ data.drone.id }}'
+ >{{ data.drone.address }}:{{ data.drone.port }}</a>
+ </th>
+ <td>
+ {% if data.running %}
+ {% trans 'Running...' %}
+ {% else %}
+ {% if data.drone.failed %}
+ {{ data.drone.failed }}
+ {% else %}
+ {% if data.summary %}
+ <table>
+ <tbody>
+ <tr class='row1'>
+ <td>Number of Successful Tasks</td><td>{{ data.summary.num_task_success }}</td>
+ </tr>
+ <tr class='row2'>
+ <td>Number of Failed Tasks</td>
+ <td>
+ {% ifequal data.summary.num_task_failed '0' %}
+ 0
+ {% else %}
+ <b style='color:red;'>{{ data.summary.num_task_failed }}</b>
+ {% endifequal %}
+ </td>
+ </tr>
+ <tr class='row1'>
+ <td>Total Duration</td>
+ <td>{{ data.summary.task_set_duration|floatformat:8}}</td>
+ </tr>
+ <tr class='row2'>
+ <td>Average Task Duration</td>
+ <td>{{ data.summary.avg_task_duration|floatformat:8}}</td>
+ </tr>
+ <tr class='row1'>
+ <td>Total Duration / # Successful Tasks</td>
+ {% ifequal data.summary.thread_count '1' %}
+ <td>{% trans 'N/A' %}</td>
+ {% else %}
+ <td>{{ data.summary.amortized_task_duration|floatformat:8 }}</td>
+ {% endifequal %}
+ </tr>
+ <tr class='row2'>
+ <td>Avg. Tasks per Second</td>
+ {% ifequal data.summary.thread_count '1' %}
+ <td>{% trans 'N/A' %}</td>
+ {% else %}
+ <td>{{ data.summary.amortized_tasks_per_second|floatformat:8 }}</td>
+ {% endifequal %}
+ </tr>
+ <tr class='row1'>
+ <td colspan='2'>
+ <div class='module'>
+ <h2>Task Details</h2>
+ <table width='100%'>
+ <thead>
+ <tr><th>Name</th><th># Runs</th><th>Avg. Duration</th></tr>
+ </thead>
+ </thead>
+ <tbody>
+ {%for info in data.summary.task_type_summary%}
+ <tr class='{% cycle row1,row2 %}'>
+ <td>{{info.task_name}}</td>
+ <td>{{info.task_count}}</td>
+ <td>{{info.task_avg_duration}}</td>
+ </tr>
+ {%endfor%}
+ </tbody>
+ </table>
+ </div>
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ {% else %}
+ {% trans 'Connected' %}
+ {% endif %}
+ {% endif %}
+ {% endif %}
+ </td>
+ </tr>
+ {% endfor %}
+ </tbody>
+ </table>
+</div>
+
--- /dev/null
+<!-- =============================================================
+ Copyright (C) 2007 King County Library System
+
+ This program is free software; you can redistribute it and/or
+ modify it under the terms of the GNU 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 General Public License for more details.
+ ================================================================= -->
+
+{% extends 'admin/index.html' %}
+{% load i18n %}
+{% block coltype %}colMS{% endblock %}
+{% block bodyclass %}dashboard{% endblock %}
+{% block breadcrumbs %}{% endblock %}
+
+{% block content %}
+ <br/>
+ <div id='content-main'>
+ {% if isRunning %}<meta http-equiv='refresh' content='3;url=/control/'/>{% endif %}
+ <div>
+ {% include 'constrictor/actions.html' %}
+ <br/>
+ {% include 'constrictor/config.html' %}
+ </div>
+ <br/>
+ {% include 'constrictor/drones.html' %}
+ </div>
+{% endblock %}
+
+{% block sidebar %}
+ <div id='content-related'>
+ <a href='/control/doc/'>Documentation</a>
+ <br/><br/>
+ <div class='module' id='recent-actions-module'>
+ <h2>Recent Actions</h2>
+ <table>
+ <tbody>
+ {% for a in actionList %}
+ <tr><td>{{ a }}</td></tr>
+ {% endfor %}
+ </tbody>
+ </table>
+ </div>
+ <br/>
+ <div class='module'>
+ <h2>Selected Test</h2>
+ <div>
+ {% for s in scripts %}
+ {% ifequal s.name selectedScript %}
+ <br/>
+ <b>{{s.name}}</b>
+ <br/><br/>
+ <div style='padding-bottom: 4px;'>{{s.description}}</div>
+ {% endifequal %}
+ {% endfor %}
+ </div>
+ </div>
+ </div>
+ </div>
+{% endblock %}
+
--- /dev/null
+<!-- =============================================================
+ Copyright (C) 2007 King County Library System
+
+ This program is free software; you can redistribute it and/or
+ modify it under the terms of the GNU 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 General Public License for more details.
+ ================================================================= -->
+
+{% extends 'admin/index.html' %}
+{% load i18n %}
+
+{% block content %}
+ <script>
+ function saveProps() {
+ if(confirm(
+ "{% trans "This will instruct each drone to write it's current properties to the local properties file." %}"))
+ location.href='../saveProps';
+ }
+ function setDesc() {
+ s = document.getElementById('prop_sel');
+ name = s.options[s.selectedIndex].value;
+ document.getElementById('prop_desc').innerHTML =
+ '<span><b>' + name + '</b><br/><br/>' + document.getElementById('desc_' + name).innerHTML + '</span>';
+ }
+ </script>
+ <div class=''>
+ <h2>Set Test Properties</h2>
+
+ <form action='/control/setProps' method='get'>
+ <table>
+ <tr>
+ <td>
+ <select id='prop_sel' name='prop' onchange='setDesc();'>
+ {% for p in properties %}
+ <option value='{{p.name}}'>{{p.name}}</option>
+ <option id='desc_{{p.name}}' style='visibility:hidden;display:none;'>{{p.description}}</option>
+ {% endfor %}
+ </select>
+ <td>
+ <td><input name='propval' type='text' size='42'/></td>
+ <td>
+ <select name='drone'>
+ <option value='all'>All Drones</option>
+ {% for data in droneData %}
+ <option value='{{data.drone.id}}'>{{data.drone}}</option>
+ {% endfor %}
+ </select>
+ </td>
+ <td><input type='submit' value='Apply'/></td>
+ </tr>
+ </table>
+ </form>
+
+ <div class='module'>
+ <h2>Test Description</h2>
+ <div id='prop_desc'> </div>
+ </div>
+ <script>setDesc();</script>
+ <br/>
+ <div>
+ <a href='javascript:saveProps()'><b>-> Persist Properties to File</b></a>
+ </div>
+ <br/><br/>
+
+ * Note: Properties that accept multiple values should be formatted as comma-separated lists
+ </div>
+{% endblock %}
+
+<!-- if we remove the sidebar block, we get a key-error on 'user'
+{% block sidebar %}
+{% endblock %}
+-->
+
--- /dev/null
+# -----------------------------------------------------------------------
+# Copyright (C) 2007-2008 King County Library System
+# Bill Erickson <erickson@esilibrary.com>
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU 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 General Public License for more details.
+# -----------------------------------------------------------------------
+from django.http import HttpResponse, HttpResponseRedirect
+from django.shortcuts import get_object_or_404, render_to_response
+from constrictor.controller import GUIControllerSet
+from constrictor.properties import Properties
+from constrictor_gui.control.models import Drone, Script, Property, Plugin
+from constrictor.log import *
+
+from django.contrib.admin.views.decorators import staff_member_required
+from django.views.decorators.cache import never_cache
+
+
+
+controllerSet = None
+MAIN_TEMPLATE = 'constrictor/index.html'
+SET_PROP_TEMPLATE = 'constrictor/props.html'
+TITLE = 'Constrictor'
+globalThreadCount = 2
+globalIterationCount = 5
+globalSelectedScript = 'sleep'
+isRunning = False
+runningBatch = None
+actionList = []
+initPropsSet = False
+
+
+def __init():
+ global controllerSet
+ if not controllerSet or controllerSet.isEmpty():
+ controllerSet = GUIControllerSet()
+ for drone in Drone.objects.all(): # filter option for enabled?
+ if drone.enabled:
+ controllerSet.createController(drone.address, drone.port, drone.id)
+
+
+def collectDroneData():
+ global controllerSet, runningBatch, isRunning
+
+ responded = {}
+ if runningBatch:
+ responded = runningBatch.recv(0)
+ if runningBatch.complete:
+ isRunning = False
+ else:
+ isRunning = False
+
+ droneData = []
+
+ for d in controllerSet.getSet():
+ info = {'drone': d, 'summary': responded.get(d.name)}
+
+ if info['summary']:
+ info['running'] = False
+ tsummary = info['summary'].get('task_type_summary')
+
+ if tsummary:
+ if not isinstance(tsummary, list): # we've already parsed this summary
+ taskInfo = tsummary.split(';')
+ info['summary']['task_type_summary'] = []
+ for t in taskInfo:
+ if not t: continue
+ tn, tc, ts = t.split(':')
+ info['summary']['task_type_summary'].append({
+ 'task_name' : tn,
+ 'task_count' : tc,
+ 'task_avg_duration' : ts
+ })
+
+ else:
+ if isRunning:
+ info['running'] = True
+ droneData.append(info)
+
+ for d in controllerSet.getFailed():
+ droneData.append({'drone':d})
+
+ return droneData
+
+
+def makeContext(action=None):
+ global controllerSet, runningBatch, isRunning
+
+ if action:
+ actionList.append(_(action))
+
+ args = {
+ 'globalIterationCount': globalIterationCount,
+ 'globalThreadCount': globalThreadCount,
+ 'droneData': collectDroneData(),
+ 'isRunning' : isRunning,
+ 'scripts' : Script.objects.all(),
+ 'selectedScript' : globalSelectedScript,
+ 'properties' : Property.objects.all(),
+ 'plugins' : Plugin.objects.all()
+ }
+
+ if actionList:
+ a = list(actionList)[-10:]
+ args['actionList'] = a
+
+ if runningBatch and not runningBatch.complete:
+ args['display'] = _('Running Tests...')
+
+ return args
+
+
+def index(request):
+ global initPropsSet
+ __init()
+ if not initPropsSet:
+ _updateBasicProps()
+ initPropsSet = True
+ return render_to_response(MAIN_TEMPLATE, makeContext())
+index = staff_member_required(never_cache(index))
+
+
+def redirect(request):
+ return HttpResponseRedirect('/control/')
+
+def reset(request):
+ global runningBatch
+ __init()
+ controllerSet.reset()
+ __init()
+ if runningBatch:
+ runningBatch.clearResponded()
+ return render_to_response(MAIN_TEMPLATE, makeContext('Reset'))
+reset = staff_member_required(never_cache(reset))
+
+
+def run(request):
+ __init()
+ global controllerSet, isRunning, runningBatch
+
+ isRunning = True
+ runningBatch = controllerSet.runBatch()
+ return render_to_response(MAIN_TEMPLATE, makeContext('Run Tests'))
+run = staff_member_required(never_cache(run))
+
+
+def editDrones(request):
+ __init()
+ return render_to_response(MAIN_TEMPLATE, makeContext('Edit Drones'))
+editDrones = staff_member_required(never_cache(editDrones))
+
+def doc(request):
+ return render_to_response('constrictor/docs.html', {})
+
+def setProps(request):
+ __init()
+
+ if request.GET.get('propval'):
+
+ prop = request.GET['prop']
+ propval = request.GET['propval']
+ drone = request.GET['drone']
+
+
+ if drone == 'all':
+ controllerSet.broadcastCommand(action='setprop', prop=prop, val=propval)
+ else:
+ controllerSet.sendCommand(drone, action='setprop', prop=prop, val=propval)
+
+ return render_to_response(SET_PROP_TEMPLATE, makeContext('Set Properties'))
+
+ else:
+ return render_to_response(SET_PROP_TEMPLATE, makeContext('Set Properties'))
+setProps = staff_member_required(never_cache(setProps))
+
+def saveProps(request):
+ __init()
+ controllerSet.broadcastCommand(action='saveprops')
+ return render_to_response(SET_PROP_TEMPLATE, makeContext('Set Properties'))
+
+saveProps = staff_member_required(never_cache(saveProps))
+
+
+
+def _updateBasicProps():
+ global globalThreadCount, globalIterationCount, globalSelectedScript
+ controllerSet.broadcastCommand(
+ action='setprop', prop='constrictor.numThreads', val=globalThreadCount)
+ controllerSet.broadcastCommand(
+ action='setprop', prop='constrictor.numIterations', val=globalIterationCount)
+ controllerSet.broadcastCommand(
+ action='setprop', prop='constrictor.script', val=globalSelectedScript+'.py')
+
+def updateBasicProps(request):
+ global globalThreadCount, globalIterationCount, globalSelectedScript
+ __init()
+
+ globalThreadCount = request.GET['threadCount']
+ globalIterationCount = request.GET['iterationCount']
+ globalSelectedScript = request.GET['selectedScript']
+ _updateBasicProps()
+
+ return render_to_response(MAIN_TEMPLATE, makeContext('Update Basic Props'))
+
+updateBasicProps = staff_member_required(updateBasicProps)
+
--- /dev/null
+#!/usr/bin/env python
+from django.core.management import execute_manager
+try:
+ import settings # Assumed to be in the same directory.
+except ImportError:
+ import sys
+ sys.stderr.write("Error: Can't find the file 'settings.py' in the directory containing %r. It appears you've customized things.\nYou'll have to run django-admin.py, passing it your settings module.\n(If the file settings.py does indeed exist, it's causing an ImportError somehow.)\n" % __file__)
+ sys.exit(1)
+
+if __name__ == "__main__":
+ execute_manager(settings)
--- /dev/null
+# Django settings for constrictor_gui project.
+
+DEBUG = True
+TEMPLATE_DEBUG = DEBUG
+
+ADMINS = (
+ # ('Your Name', 'your_email@domain.com'),
+)
+
+MANAGERS = ADMINS
+
+DATABASE_ENGINE = 'sqlite3' # 'postgresql_psycopg2', 'postgresql', 'mysql', 'sqlite3' or 'ado_mssql'.
+DATABASE_NAME = 'django.db' # Or path to database file if using sqlite3.
+DATABASE_USER = '' # Not used with sqlite3.
+DATABASE_PASSWORD = '' # Not used with sqlite3.
+DATABASE_HOST = '' # Set to empty string for localhost. Not used with sqlite3.
+DATABASE_PORT = '' # Set to empty string for default. Not used with sqlite3.
+
+# Local time zone for this installation. Choices can be found here:
+# http://www.postgresql.org/docs/8.1/static/datetime-keywords.html#DATETIME-TIMEZONE-SET-TABLE
+# although not all variations may be possible on all operating systems.
+# If running in a Windows environment this must be set to the same as your
+# system time zone.
+TIME_ZONE = 'America/New_York'
+
+# Language code for this installation. All choices can be found here:
+# http://www.w3.org/TR/REC-html40/struct/dirlang.html#langcodes
+# http://blogs.law.harvard.edu/tech/stories/storyReader$15
+LANGUAGE_CODE = 'en-us'
+
+SITE_ID = 1
+
+# If you set this to False, Django will make some optimizations so as not
+# to load the internationalization machinery.
+USE_I18N = True
+
+# Absolute path to the directory that holds media.
+# Example: "/home/media/media.lawrence.com/"
+MEDIA_ROOT = ''
+
+# URL that handles the media served from MEDIA_ROOT. Make sure to use a
+# trailing slash if there is a path component (optional in other cases).
+# Examples: "http://media.lawrence.com", "http://example.com/media/"
+MEDIA_URL = ''
+
+# URL prefix for admin media -- CSS, JavaScript and images. Make sure to use a
+# trailing slash.
+# Examples: "http://foo.com/media/", "/media/".
+ADMIN_MEDIA_PREFIX = '/media/'
+
+# Make this unique, and don't share it with anybody.
+SECRET_KEY = '8q9a02!=p-5esy_o2_**v+xmbgen5bi%jkc1v=0##py6s=q^0d'
+
+# List of callables that know how to import templates from various sources.
+TEMPLATE_LOADERS = (
+ 'django.template.loaders.filesystem.load_template_source',
+ 'django.template.loaders.app_directories.load_template_source',
+# 'django.template.loaders.eggs.load_template_source',
+)
+
+MIDDLEWARE_CLASSES = (
+ 'django.middleware.common.CommonMiddleware',
+ 'django.contrib.sessions.middleware.SessionMiddleware',
+ 'django.contrib.auth.middleware.AuthenticationMiddleware',
+ 'django.middleware.doc.XViewMiddleware',
+)
+
+ROOT_URLCONF = 'constrictor_gui.urls'
+
+import os.path
+TEMPLATE_DIRS = (
+ 'control/templates',
+ #os.path.join(os.path.basename(__file__), 'templates')
+ # Put strings here, like "/home/html/django_templates" or "C:/www/django/templates".
+ # Always use forward slashes, even on Windows.
+ # Don't forget to use absolute paths, not relative paths.
+)
+
+INSTALLED_APPS = (
+ 'django.contrib.auth',
+ 'django.contrib.contenttypes',
+ 'django.contrib.sessions',
+ 'django.contrib.sites',
+ 'django.contrib.admin',
+ 'constrictor_gui.control',
+)
+
+
+import sys, os
+#print os.path.join(os.path.basename(__file__), '/../')
+#sys.path.append(os.path.join(os.path.basename(__file__), '/../'))
+
+# append the parent directory to the python path
+sys.path.append(os.path.join(os.path.abspath(''), '..'))
+
--- /dev/null
+from django.conf.urls.defaults import *
+
+urlpatterns = patterns('',
+ (r'^$', 'constrictor_gui.control.views.redirect'),
+ (r'^control/$', 'constrictor_gui.control.views.index'),
+ (r'^control/reset/$', 'constrictor_gui.control.views.reset'),
+ (r'^control/run/$', 'constrictor_gui.control.views.run'),
+# (r'^control/updateThreads/$', 'constrictor_gui.control.views.updateThreads'),
+# (r'^control/updateIterations/$', 'constrictor_gui.control.views.updateIterations'),
+ (r'^control/editDrones/$', 'constrictor_gui.control.views.editDrones'),
+ (r'^control/doc/$', 'constrictor_gui.control.views.doc'),
+# (r'^control/setScript/$', 'constrictor_gui.control.views.setScript'),
+ (r'^control/updateBasicProps/$', 'constrictor_gui.control.views.updateBasicProps'),
+ (r'^control/setProps/$', 'constrictor_gui.control.views.setProps'),
+ (r'^control/saveProps/$', 'constrictor_gui.control.views.saveProps'),
+ (r'^admin/', include('django.contrib.admin.urls')),
+)
--- /dev/null
+<?xml version='1.0'?>
+
+<!-- Evergreen script -->
+
+<constrictor plugin='evergreen' xmlns='http://esilibrary.com/namespaces/constrictor/v1'>
+
+ <!-- General description of this module -->
+ <desc>
+ Test bundle for the Evergreen Open Source Integrated Library System (OpenILS).
+ </desc>
+
+
+ <!--
+ extended properties used by this module.
+ Those with the "publish" flag set to "true" will be
+ accessible by the controller module
+ -->
+ <properties>
+ <property name='evergreen.server' publish='true'>
+ <desc>The hostname or IP address of the Evergreen server</desc>
+ </property>
+ <property name='evergreen.gatewayPath' publish='true'>
+ <desc>The URL base path the Evergreen gateway</desc>
+ </property>
+ <property name='evergreen.netProtocol' publish='true'>
+ <desc>The communication protocol used with the Evergreen gateway. Choices are XML and JSON.</desc>
+ </property>
+ <property name='evergreen.username' publish='true'>
+ <desc>The login name of the Evergreen user running the tests</desc>
+ </property>
+ <property name='evergreen.password' publish='true'>
+ <desc>The password of the Evergreen user running the tests</desc>
+ </property>
+ <property name='evergreen.workstation' publish='true'>
+ <desc>
+ The name of the workstation where tests should be run from.
+ The workstation is used to define "where" a test is occuring.
+ </desc>
+ </property>
+ <property name='evergreen.copyBarcodes' publish='true'>
+ <desc>Comma-separated list of asset.copy (copy) barcodes used in tests</desc>
+ </property>
+ <property name='evergreen.titleIDs' publish='true'>
+ <desc>Comma-separated list of biblio.record_entry (title) IDs for tests</desc>
+ </property>
+ <!--
+ <property name='evergreen.patronBarcodes' publish='true'>
+ <desc>Comma-separated list of actor.card (user) barcodes for tests</desc>
+ </property>
+ -->
+ <property name='evergreen.patronIDs' publish='true'>
+ <desc>Comma-separated list of actor.usr (user) IDs for tests</desc>
+ </property>
+ <property name='evergreen.orgIDs' publish='true'>
+ <desc>Comma-separated list of actor.org_unit (Org Unit) IDs for tests</desc>
+ </property>
+ <property name='evergreen.osrfConfig' publish='true'>
+ <desc>Path to the opensrf config file. Only necessary if evergreen.netProtocol is set to jabber</desc>
+ </property>
+ <property name='evergreen.osrfConfigContext' publish='true'>
+ <desc>The opensrf config file context path. Only necessary if evergreen.netProtocol is set to jabber</desc>
+ </property>
+ </properties>
+
+
+ <!--
+ Set of test scripts defined by this module
+ Those with the "publish" flag set to "true" will be
+ accessible by the controller module
+ -->
+ <scripts>
+ <script name='eg_checkout' publish='true'>
+ <desc>
+ Performs a checkout.
+ Requires unique evergreen.copyBarcodes.
+ Optional evergreen.patronIDs for defining the checkout patron -- defaults to logged in user.
+ </desc>
+ </script>
+ <script name='eg_renew' publish='true'>
+ <desc>
+ Performs a renewal.
+ Requires unique evergreen.copyBarcodes.
+ Optional evergreen.patronIDs for defining the checkout patron -- defaults to logged in user.
+ </desc>
+ </script>
+ <script name='eg_checkin' publish='true'>
+ <desc>
+ Performs a checkin. This cancels any transits
+ created by the checkin.
+ Requires unique evergreen.copyBarcodes.
+ Optional evergreen.patronIDs for defining the checkout patron -- defaults to logged in user.
+ </desc>
+ </script>
+ <script name='eg_checkout_roundtrip' publish='true'>
+ <desc>
+ Performs a full round trip checkout and checkin. Monitored Tasks
+ include PermitCheckout, Checkout, and Checkin.
+ Requires unique evergreen.copyBarcodes.
+ Optional evergreen.patronIDs for defining the checkout patron -- defaults to logged in user.
+ </desc>
+ </script>
+ <script name='eg_title_hold' publish='true'>
+ <desc>
+ Places a title-level hold by first making the hold
+ permit call. If the permit call succeeds, the hold
+ is then created.
+ Requires evergreen.titleIDs.
+ Optional evergreen.orgIDs for defining a different hold pickup lib.
+ </desc>
+ </script>
+ </scripts>
+
+</constrictor>
--- /dev/null
+from constrictor.script import Script
+import eg_utils
+from eg_data import *
+from eg_workflow import *
+
+eg_utils.init()
+
+class CheckinScript(Script):
+
+ def onThreadInit(self, scriptThread):
+ eg_utils.initThread()
+
+ def run(self):
+
+ dm = DataManager()
+ copyBarcode = dm.getThreadData(PROP_COPY_BARCODES)
+
+ evt = doCheckin(copyBarcode)
+ if not evt: return False
+
+ScriptManager.go(CheckinScript())
+
--- /dev/null
+from constrictor.script import Script
+from constrictor.log import *
+
+import eg_utils
+from eg_data import *
+from eg_workflow import *
+
+eg_utils.init()
+
+
+class CheckoutScript(Script):
+
+ def onThreadInit(self, scriptThread):
+ eg_utils.initThread()
+
+ def run(self):
+
+ dm = DataManager()
+ patronID = dm.getThreadData(PROP_PATRON_IDS)
+ copyBarcode = dm.getThreadData(PROP_COPY_BARCODES, True)
+
+ evt = doCheckoutPermit(copyBarcode, patronID)
+ if not evt: return False
+
+ evt = doCheckout(copyBarcode, patronID, evt['payload'])
+ if not evt: return False
+
+ return True
+
+ScriptManager.go(CheckoutScript())
+
+
--- /dev/null
+from constrictor.script import Script
+from constrictor.log import *
+
+import eg_utils
+from eg_data import *
+from eg_workflow import *
+
+eg_utils.init()
+
+
+class CheckoutRoundtripScript(Script):
+
+ def onThreadInit(self, scriptThread):
+ eg_utils.initThread()
+
+ def run(self):
+
+ dm = DataManager()
+ patronID = dm.getThreadData(PROP_PATRON_IDS)
+ copyBarcode = dm.getThreadData(PROP_COPY_BARCODES, True)
+
+ evt = doCheckoutPermit(copyBarcode, patronID)
+ if not evt: return False
+
+ evt = doCheckout(copyBarcode, patronID, evt['payload'])
+ if not evt: return False
+
+ evt = doCheckin(copyBarcode)
+ if not evt: return False
+
+ return True
+
+ScriptManager.go(CheckoutRoundtripScript())
+
+
--- /dev/null
+from constrictor.properties import Properties
+from constrictor.script import ScriptThread
+from constrictor.log import *
+from oils.utils.utils import unique
+
+PROP_USERNAME = 'evergreen.username'
+PROP_PASSWORD = 'evergreen.password'
+PROP_WORKSTATION = 'evergreen.workstation'
+PROP_COPY_BARCODES = 'evergreen.copyBarcodes'
+PROP_TITLE_IDS = 'evergreen.titleIDs'
+PROP_PATRON_BARCODES = 'evergreen.patronBarcodes'
+PROP_ORG_IDS = 'evergreen.orgIDs'
+PROP_PATRON_IDS = 'evergreen.patronIDs'
+
+PROP_CONSTRICTOR_THREADS = 'constrictor.numThreads'
+
+class DataManagerException(Exception):
+ pass
+
+class DataManager(object):
+ ''' This module manages a global cache of test data '''
+
+ def __init__(self):
+ self.data = {}
+ self.props = Properties.getProperties()
+ self.readProps()
+ logDebug(self)
+
+ def __str__(self):
+ s = 'DataManager() read properties:\n'
+ for p in [
+ PROP_PASSWORD,
+ PROP_WORKSTATION,
+ PROP_COPY_BARCODES,
+ PROP_TITLE_IDS,
+ PROP_PATRON_BARCODES,
+ PROP_PATRON_IDS,
+ PROP_ORG_IDS ]:
+
+ s += "\t%s=%s\n" % (p, str(self.data[p]))
+ return s
+
+
+ def readProps(self):
+ self.readProp(PROP_USERNAME)
+ self.readProp(PROP_PASSWORD)
+ self.readProp(PROP_WORKSTATION)
+ self.readProp(PROP_COPY_BARCODES, True)
+ self.readProp(PROP_TITLE_IDS, True)
+ self.readProp(PROP_PATRON_BARCODES, True)
+ self.readProp(PROP_PATRON_IDS, True)
+ self.readProp(PROP_ORG_IDS, True)
+
+ def readProp(self, prop, split=False):
+ v = self.props.getProperty(prop)
+ if split and v:
+ v = unique(v.split(','))
+ self.data[prop] = v
+ logDebug("DataManager set property %s => %s" % (prop, str(v)))
+
+ def getThreadData(self, prop, noSharing=False):
+ ''' If the caller is requesting array-based data, we want to de-multiplex(?)
+ the requested data based on the currently running thread. In other
+ words, each thread should have its own view into the array of data
+ so that different threads are not sharing data. If there are more
+ running threads than unique points of data, then sharing is inevitable,
+ and it's assumed that's OK unless noSharing is set to true, in which
+ case an exception is raised. For atomic data, the value is returned
+ and no thread data is checked '''
+
+ data = self.data[prop]
+ if not isinstance(data, list):
+ return data
+
+ currentThread = ScriptThread.getThreadID()
+ totalThreads = self.props.getProperty(PROP_CONSTRICTOR_THREADS)
+
+ if len(data) > currentThread:
+ return data[currentThread]
+
+ if noSharing:
+ raise DataManagerException(
+ "Too many threads for unique data. Thread index is %d, size of dataset is %d" % (
+ currentThread, len(data)))
+
+ # data sharing is OK
+ return data[currentThread % len(data)]
+
--- /dev/null
+from constrictor.task import Task
+from constrictor.script import Script, ScriptManager
+from constrictor.log import *
+from osrf.gateway import XMLGatewayRequest
+import eg_utils
+
+SERVICE = 'open-ils.actor'
+METHOD = 'open-ils.actor.groups.tree.retrieve'
+
+
+class FetchUserGroupsTask(Task):
+
+ def __init__(self):
+ Task.__init__(self, self.__class__.__name__)
+
+ def run(self, **kwargs):
+ request = XMLGatewayRequest(SERVICE, METHOD)
+ return request.send()
+
+
+eg_utils.init()
+fetchGroupsTask = FetchUserGroupsTask().wrap()
+
+
+class FetchUserGroupsScript(Script):
+ def run(self):
+ res = fetchGroupsTask.run()
+ logInfo('Fetched group tree with root "%s"' % res.name())
+ return True
+
+ScriptManager.go(FetchUserGroupsScript())
+
+
+
--- /dev/null
+from constrictor.script import Script
+from constrictor.log import *
+
+import eg_utils
+from eg_data import *
+from eg_workflow import *
+
+eg_utils.init()
+
+
+class RenewScript(Script):
+
+ def onThreadInit(self, scriptThread):
+ eg_utils.initThread()
+
+ def run(self):
+
+ dm = DataManager()
+ copyBarcode = dm.getThreadData(PROP_COPY_BARCODES, True)
+
+ evt = doRenew(copyBarcode)
+ if not evt: return False
+
+ return True
+
+
+ScriptManager.go(RenewScript())
+
+
--- /dev/null
+from constrictor.task import Task
+from constrictor.log import *
+from osrf.gateway import XMLGatewayRequest
+import eg_utils
+from oils.const import *
+from osrf.net_obj import *
+
+TASKS = {}
+
+def registerTask(task):
+ TASKS[task.name] = task.wrap()
+
+class AbstractMethodTask(Task):
+ ''' Generic superclass for tasks that perform a single
+ server-side operation '''
+
+ def __init__(self, name=None):
+ if not name:
+ name = self.__class__.__name__
+ Task.__init__(self, name)
+ self.service = None
+ self.method = None
+
+ def runMethod(self, *args):
+ return eg_utils.request(self.service, self.method, *args).send()
+
+ def register(self):
+ TASKS[self.name] = self.wrap()
+
+class CheckoutPermitTask(AbstractMethodTask):
+ def __init__(self):
+ AbstractMethodTask.__init__(self)
+ self.service = OILS_APP_CIRC
+ self.method = 'open-ils.circ.checkout.permit'
+
+ def run(self, **kw):
+ ''' kw[copy_barcode] The item barcode
+ kw[patron_id] The user's id '''
+
+ return self.runMethod(eg_utils.authtoken(), dict(kw))
+
+registerTask(CheckoutPermitTask())
+
+
+class CheckoutTask(AbstractMethodTask):
+
+ def __init__(self):
+ AbstractMethodTask.__init__(self)
+ self.service = OILS_APP_CIRC
+ self.method = 'open-ils.circ.checkout'
+
+ def run(self, **kw):
+ ''' kw[copy_barcode] The item barcode
+ kw[patron_id] The user's id
+ kw[permit_key] The key returned by checkout permit '''
+
+ return self.runMethod(eg_utils.authtoken(), dict(kw))
+
+registerTask(CheckoutTask())
+
+
+class RenewTask(AbstractMethodTask):
+ def __init__(self):
+ AbstractMethodTask.__init__(self)
+ self.service = OILS_APP_CIRC
+ self.method = 'open-ils.circ.renew'
+
+ def run(self, **kw):
+ ''' kw[copy_barcode] The item barcode '''
+
+ return self.runMethod(eg_utils.authtoken(), dict(kw))
+
+registerTask(RenewTask())
+
+
+class CheckinTask(AbstractMethodTask):
+
+ def __init__(self):
+ AbstractMethodTask.__init__(self)
+ self.service = OILS_APP_CIRC
+ self.method = 'open-ils.circ.checkin'
+
+ def run(self, **kw):
+ ''' kw[copy_barcode] '''
+ return self.runMethod(eg_utils.authtoken(), dict(kw))
+
+registerTask(CheckinTask())
+
+
+class AbortTransitTask(AbstractMethodTask):
+ def __init__(self):
+ AbstractMethodTask.__init__(self)
+ self.service = OILS_APP_CIRC
+ self.method = 'open-ils.circ.transit.abort'
+
+ def run(self, **kw):
+ ''' kw[copy_barcode] '''
+ return self.runMethod(eg_utils.authtoken(), {"barcode":kw['copy_barcode']})
+
+registerTask(AbortTransitTask())
+
+class TitleHoldPermitTask(AbstractMethodTask):
+ def __init__(self):
+ AbstractMethodTask.__init__(self)
+ self.service = OILS_APP_CIRC
+ self.method = 'open-ils.circ.title_hold.is_possible'
+
+ def run(self, **kw):
+ ''' kw[title_id]
+ kw[patron_id]
+ kw[pickup_lib]
+ '''
+ return self.runMethod(eg_utils.authtoken(), {
+ "patronid" : kw['patron_id'],
+ "titleid" : kw['title_id'],
+ "pickup_lib" : kw['pickup_lib']})
+
+registerTask(TitleHoldPermitTask())
+
+
+
+class TitleHoldTask(AbstractMethodTask):
+ def __init__(self):
+ AbstractMethodTask.__init__(self)
+ self.service = OILS_APP_CIRC
+ self.method = 'open-ils.circ.holds.create'
+
+ def run(self, **kw):
+ ''' kw[title_id]
+ kw[patron_id]
+ kw[pickup_lib]
+ '''
+ # construct the hold object
+ hold = osrfNetworkObject.ahr()
+ hold.pickup_lib(kw['pickup_lib'])
+ hold.usr(kw['patron_id'])
+ hold.target(kw['title_id'])
+ hold.hold_type('T')
+
+ return self.runMethod(eg_utils.authtoken(), hold)
+
+registerTask(TitleHoldTask())
+
+
+class TitleHoldCancelTask(AbstractMethodTask):
+ def __init__(self):
+ AbstractMethodTask.__init__(self)
+ self.service = OILS_APP_CIRC
+ self.method = 'open-ils.circ.hold.cancel'
+
+ def run(self, **kw):
+ ''' kw['hold_id'] '''
+ return self.runMethod(eg_utils.authtoken(), kw['hold_id'])
+
+registerTask(TitleHoldCancelTask())
+
+
+class TitleHoldFetchAllTask(AbstractMethodTask):
+
+ def __init__(self):
+ AbstractMethodTask.__init__(self)
+ self.service = OILS_APP_CIRC
+ self.method = 'open-ils.circ.holds.id_list.retrieve'
+
+ def run(self, **kw):
+ ''' kw['patron_id'] '''
+ return self.runMethod(eg_utils.authtoken(), kw['patron_id'])
+
+registerTask(TitleHoldFetchAllTask())
--- /dev/null
+from constrictor.script import Script, ScriptThread
+import eg_utils
+from eg_data import *
+from eg_workflow import *
+
+eg_utils.init()
+
+
+class CreateTitleHoldScript(Script):
+
+ def onThreadInit(self, scriptThread):
+ # collect all of the holds the current thread user already has
+ # so any new holds can be cancelled after the thread is complete
+ eg_utils.initThread()
+ dm = DataManager()
+ patronID = dm.getThreadData(PROP_PATRON_IDS)
+ scriptThread.userData = doTitleHoldFetchAll(patronID)
+ logDebug("init: grabbed existing holds %s" % str([ int(i) for i in scriptThread.userData]))
+
+
+ def run(self):
+
+ dm = DataManager()
+ titleID = dm.getThreadData(PROP_TITLE_IDS)
+ pickupLib = dm.getThreadData(PROP_ORG_IDS)
+ patronID = dm.getThreadData(PROP_PATRON_IDS)
+
+ doTitleHold(titleID, patronID, pickupLib)
+
+ # XXX TODO update EG to return hold IDs for new holds so
+ # that this script does not have to manually sort through
+ # the holds to find what to clean up
+
+ # go ahead and cancel any new holds
+ allHolds = doTitleHoldFetchAll(patronID)
+ logDebug("grabbed all holds %s" % str([ int(i) for i in allHolds]))
+
+ scriptThread = ScriptThread.currentScriptThread()
+ cancelHolds = [ h for h in allHolds if h not in scriptThread.userData ]
+
+ for hold in cancelHolds:
+ doTitleHoldCancel(hold)
+
+
+
+ScriptManager.go(CreateTitleHoldScript())
+
--- /dev/null
+from constrictor.properties import Properties
+from constrictor.log import *
+from osrf.net_obj import NetworkObject
+import osrf.json
+from osrf.gateway import GatewayRequest, XMLGatewayRequest, JSONGatewayRequest
+from oils.utils.idl import IDLParser
+from oils.utils.utils import eventText, eventCode, md5sum
+from oils.const import *
+import os, errno
+
+props = Properties.getProperties()
+
+def init():
+
+ loadIDL()
+ initOsrf()
+
+ if props.getProperty('evergreen.autologin') == 'true':
+ login(
+ props.getProperty('evergreen.username'),
+ props.getProperty('evergreen.password'),
+ props.getProperty('evergreen.workstation'))
+
+ user = None
+ if not props.getProperty('evergreen.patronIDs'):
+ # if there are not configured patron IDs, go ahead
+ # and use the ID of the logged in user
+ user = fetchSessionUser()
+ logInfo("Setting evergreen.patronIDs to logged in user %s" % str(user.id()))
+ props.setProperty('evergreen.patronIDs', str(user.id()))
+
+ if not props.getProperty('evergreen.orgIDs'):
+ # simlilarly, if no org is provided, use the home org of the logged in user
+ if not user:
+ user = fetchSessionUser()
+ logInfo("Setting evergreen.orgIDs to logged in user's home_ou %s" % str(user.home_ou()))
+ props.setProperty('evergreen.orgIDs', str(user.home_ou()))
+
+
+
+
+def initOsrf():
+ # if necessary, create a connection to the opensrf network for this thread
+ if str(props.getProperty('evergreen.netProtocol')).lower() == 'jabber':
+ if props.getProperty('evergreen.osrfConfig'):
+ logInfo("Connecting to the opensrf network")
+ from osrf.system import osrfConnect
+ osrfConnect(
+ props.getProperty('evergreen.osrfConfig'),
+ props.getProperty('evergreen.osrfConfigContext'))
+
+
+def initThread():
+ ''' Performs thread-specific initialization '''
+ initOsrf()
+
+def fetchSessionUser():
+ user = request('open-ils.auth', 'open-ils.auth.session.retrieve', authtoken()).send()
+ if eventCode(user):
+ raise ILSEventException(osrf.json.to_json(user))
+ logInfo("fetched user %s" % user.usrname())
+ return user
+
+def loadIDL():
+
+ # XX add logic to allow IDL fetching via jabber
+
+ server = props.getProperty('evergreen.server')
+ cacheDir = props.getProperty('constrictor.cacheDir')
+ GatewayRequest.setDefaultHost(server)
+
+ import urllib2
+ parser = IDLParser()
+ file = None
+ filePath = '%s/evergreen/fm_IDL.xml' % cacheDir
+
+ try:
+ # see if we have a local copy of the IDL already in the cache
+ file = open(filePath, 'r')
+
+ except IOError:
+ logInfo('fetching: http://%s/%s' % (server, props.getProperty('evergreen.IDLPath')))
+ f = urllib2.urlopen('http://%s/%s' % (server, props.getProperty('evergreen.IDLPath')))
+
+ if not os.path.exists('%s/evergreen' % cacheDir):
+ os.mkdir('%s/evergreen' % cacheDir)
+
+ file = open(filePath, 'w')
+ file.write(f.read())
+ file.close()
+
+ logInfo("parsing Evergreen IDL file...")
+ parser.setIDL(filePath)
+ parser.parseIDL()
+
+
+class AtomicReqWrapper(object):
+ ''' This wraps the built-in osrfAtomicRequest in a
+ gateway request-style class interface '''
+
+ def __init__(self, service, method, *args):
+ self.service = service
+ self.method = method
+ self.args = list(args)
+
+ def send(self):
+ from osrf.ses import osrfAtomicRequest
+ return osrfAtomicRequest(self.service, self.method, *(self.args))
+
+
+
+def request(service, method, *args):
+ global props
+ proto = props.getProperty('evergreen.netProtocol')
+ if str(proto).lower() == 'jabber':
+ req = AtomicReqWrapper(service, method, *args)
+ else:
+ if str(proto).lower() == 'json':
+ req = JSONGatewayRequest(service, method, *args)
+ else:
+ req = XMLGatewayRequest(service, method, *args)
+ req.setPath(props.getProperty('evergreen.gatewayPath'))
+ return req
+
+
+__authtoken = None
+def authtoken():
+ if not __authtoken:
+ raise AuthException()
+ return __authtoken
+
+def login(username, password, workstation=None):
+ ''' Login to the server and get back an authtoken'''
+ global __authtoken
+
+ logInfo("attempting login with user " + username)
+
+ seed = request(
+ 'open-ils.auth',
+ 'open-ils.auth.authenticate.init', username).send()
+
+ # generate the hashed password
+ password = md5sum(seed + md5sum(password))
+
+ result = request(
+ 'open-ils.auth',
+ 'open-ils.auth.authenticate.complete',
+ { 'workstation' : workstation,
+ 'username' : username,
+ 'password' : password,
+ 'type' : 'staff'
+ }).send()
+
+ if eventText(result) != OILS_EVENT_SUCCESS:
+ raise AuthException(eventText(result))
+
+ __authtoken = result['payload']['authtoken']
+ return __authtoken
+
+
+class AuthException(Exception):
+ def __init__(self, msg=''):
+ self.msg = msg
+ def __str__(self):
+ return 'AuthException: %s' % self.msg
+
+class ILSEventException(Exception):
+ pass
+
--- /dev/null
+from eg_tasks import TASKS
+from eg_data import DataManager
+from eg_utils import ILSEventException
+from constrictor.log import *
+from oils.utils.utils import eventText, eventCode
+from oils.const import *
+from osrf.json import osrfObjectToJSON
+
+SUCCESS = {'textcode':'SUCCESS', 'ilsevent':0}
+
+def doCheckoutPermit(copyBarcode, patronID, recurse=False):
+
+ evt = TASKS['CheckoutPermitTask'].run(copy_barcode=copyBarcode, patron_id=patronID)
+ logInfo("CheckoutPermit(%s, %s) -> %s" % (copyBarcode, patronID, eventText(evt)))
+
+ if eventText(evt) == OILS_EVENT_SUCCESS:
+ return evt
+
+ if recurse: return None
+
+ if eventText(evt) == 'OPEN_CIRCULATION_EXISTS':
+ evt = doCheckin(copyBarcode)
+ if eventText(evt) == OILS_EVENT_SUCCESS:
+ return doCheckoutPermit(copyBarcode, patronID, True)
+ logInfo("* Unable to checkin open circ: %s" % copyBarcode)
+
+ return None
+
+
+def doCheckout(copyBarcode, patronID, permitKey):
+
+ evt = TASKS['CheckoutTask'].run(
+ copy_barcode=copyBarcode, patron_id=patronID, permit_key=permitKey)
+ logInfo("Checkout(%s,%s) -> %s" % (copyBarcode, patronID, eventText(evt)))
+
+ if eventText(evt) == OILS_EVENT_SUCCESS:
+ return evt
+
+ return None
+
+
+def doRenew(copyBarcode):
+
+ evt = TASKS['RenewTask'].run(copy_barcode=copyBarcode)
+ logInfo("Checkout(%s) -> %s" % (copyBarcode, eventText(evt)))
+
+ if eventText(evt) == OILS_EVENT_SUCCESS:
+ return evt
+
+ return None
+
+
+
+def doCheckin(copyBarcode):
+
+ evt = TASKS['CheckinTask'].run(copy_barcode=copyBarcode)
+ logInfo("Checkin(%s) -> %s" % (copyBarcode, eventText(evt)))
+
+ if eventText(evt) == OILS_EVENT_SUCCESS or eventText(evt) == 'NO_CHANGE':
+ return SUCCESS
+
+ if eventText(evt) == 'ROUTE_ITEM':
+
+ logInfo("Cancelling post-checkin transit...")
+ evt = TASKS['AbortTransitTask'].run(copy_barcode=copyBarcode)
+
+ if eventCode(evt): # transit returns "1" on success
+ logError("Unable to abort transit for %s : %s" % (copyBarcode, osrfObjectToJSON(evt)))
+ return None
+
+ return SUCCESS
+
+ return None
+
+
+
+def doTitleHoldPermit(titleID, patronID, pickupLib):
+
+ evt = TASKS['TitleHoldPermitTask'].run(
+ title_id=titleID, patron_id=patronID, pickup_lib=pickupLib)
+
+ if eventCode(evt):
+ raise ILSEventException(
+ "TitleHoldPermit(%s, %s, %s) failed -> %s" % (
+ titleID, patronID, pickupLib, osrfObjectToJSON(evt)))
+
+ if str(evt) != '1':
+ logInfo("TitleHoldPermit(%s, %s, %s) not allowed -> %s" % (
+ titleID, patronID, pickupLib, evt))
+ return None
+
+ logInfo('TitleHoldPermit(%s, %s, %s) -> SUCCESS' % (titleID, patronID, pickupLib))
+ return True
+
+
+def doTitleHold(titleID, patronID, pickupLib):
+
+ if not doTitleHoldPermit(titleID, patronID, pickupLib):
+ return
+
+ evt = TASKS['TitleHoldTask'].run(title_id=titleID, patron_id=patronID, pickup_lib=pickupLib)
+
+ if isinstance(evt, list):
+ evts = []
+ for e in evt:
+ evts.append(e['textcode'])
+ logInfo("TitleHold(%s, %s, %s) -> %s" % (titleID, patronID, pickupLib, str(evts)))
+ return None
+
+ if eventCode(evt):
+ logInfo("TitleHold(%s, %s, %s) -> %s" % (titleID, patronID, pickupLib, eventCode(evt)))
+ return None
+
+ if str(evt) != '1':
+ logInfo("TitleHold(%s, %s, %s) placement failed: %s" % (
+ titleID, patronID, pickupLib, str(evt)))
+ return None
+
+ logInfo('TitleHold(%s, %s, %s) -> SUCCESS' % (titleID, patronID, pickupLib))
+ return True
+
+def doTitleHoldCancel(holdID):
+
+ evt = TASKS['TitleHoldCancelTask'].run(hold_id=holdID)
+
+ if eventCode(evt):
+ if eventText(evt) == 'ACTION_HOLD_REQUEST_NOT_FOUND':
+ return True
+ raise ILSEventException(
+ "TitleHoldCancel(%s) failed -> %s" % (holdID, str(evt)))
+
+ logInfo('TitleHoldCancel(%s) -> SUCCESS' % holdID)
+ return True
+
+def doTitleHoldFetchAll(patronID):
+ evt = TASKS['TitleHoldFetchAllTask'].run(patron_id=patronID)
+ if eventCode(evt):
+ raise ILSEventException(
+ "TitleHoldFetchAll(%s) failed -> %s" % (patronID, evt))
+ return evt
+
+
--- /dev/null
+#!/usr/bin/python
+# -----------------------------------------------------------------------
+# Copyright (C) 2007-2008 King County Library System
+# Bill Erickson <erickson@esilibrary.com>
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU 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 General Public License for more details.
+# -----------------------------------------------------------------------
+
+import os, sys, getopt, re, constrictor.properties
+
+# tell django where to find the settings file
+os.environ['DJANGO_SETTINGS_MODULE'] = 'constrictor_gui.settings'
+
+from django.contrib.auth.models import User
+from constrictor_gui.control.models import Drone, Script, Plugin, Property
+from constrictor.utils import loadProps
+import constrictor_gui.settings as settings
+from constrictor.log import *
+import xml.dom.minidom
+
+
+ops, args = getopt.getopt(sys.argv[1:], 'fu')
+options = dict( (k,v) for k,v in ops )
+
+
+def buildDjangoDB():
+
+ ''' Re-synchronizes the Django db from the django models.
+ re-creates the admin user.
+ If there are no drones configured, we create a default drone at 127.0.0.1 '''
+
+ global options
+ if not options.has_key('-f'):
+ r = raw_input('This will destroy the existing django database. continue? [yes/no] ')
+ if r.lower() != 'yes':
+ sys.exit(0)
+
+ if os.path.exists(settings.DATABASE_NAME):
+ os.remove(settings.DATABASE_NAME)
+
+ if os.system('python manage.py --noinput syncdb') != 0:
+ # tell django to build the base tables
+ sys.stderr.write('Error syncing database...')
+ sys.exit(1)
+
+ # create the admin user
+ admin = User.objects.create_user('admin', 'admin@example.org', 'constrictor')
+ admin.is_staff = True
+ admin.is_superuser = True
+ admin.save()
+
+ # create a default drone if there are none
+ if len(Drone.objects.all()) == 0:
+ Drone(address='127.0.0.1', port=constrictor.utils.DEFAULT_PORT, enabled=True).save()
+
+
+def getXMLAttr(node, name, ns=None):
+ for (k, v) in node.attributes.items():
+ if k == name:
+ return v
+ return None
+
+
+
+def loadModuleConfigs():
+ ''' Returns the DOM nodes for the XML configs if a config is found '''
+
+ props = constrictor.properties.Properties.getProperties()
+ scriptDirs = props.getProperty('constrictor.scriptDirs').split(',')
+ configs = []
+ for d in scriptDirs:
+ conf = None
+ try:
+ confFile = "%s/%s" % (d, constrictor.utils.CONFIG_FILENAME)
+ print "Parsing config file: " + confFile
+ conf = xml.dom.minidom.parse(confFile)
+ configs.append(conf)
+ except:
+ continue
+ return configs
+
+def updateDjangoPlugins():
+ configs = loadModuleConfigs()
+
+ for conf in configs:
+ name = getXMLAttr(conf.documentElement, 'plugin')
+ desc = conf.documentElement.getElementsByTagName('desc')[0]
+ desc = desc.childNodes[0].nodeValue
+ print "Registering plugin %s" % name
+ plugin = Plugin(name=name, description=desc)
+ plugin.save()
+
+ updateDjangoScripts(plugin, conf)
+ updateDjangoProperties(plugin, conf)
+
+def updateDjangoScripts(plugin, conf):
+
+ for scriptNode in conf.getElementsByTagName('script'):
+ # read the scripts from the config and insert them into
+ # the django db if publish is true
+
+ if getXMLAttr(scriptNode, 'publish')+''.lower() != 'true':
+ continue
+ name = getXMLAttr(scriptNode, 'name')
+
+ # see if this script already exists in the database
+ existing = Script.objects.filter(name=name)
+ if len(existing) > 0: continue
+
+ print 'Registering script "%s" for module "%s"' % (name, plugin.name)
+
+ #if it does not exist in the db, create it
+ desc = scriptNode.getElementsByTagName('desc')[0]
+ if desc and len(desc.childNodes) > 0:
+ desc = desc.childNodes[0].nodeValue
+
+ script = Script(plugin_id=plugin.id, name=name, description=desc or '')
+ script.save()
+
+def updateDjangoProperties(plugin, conf):
+
+ for propNode in conf.getElementsByTagName('property'):
+
+ if getXMLAttr(propNode, 'publish')+''.lower() != 'true':
+ continue
+
+ name = getXMLAttr(propNode, 'name')
+ existing = Property.objects.filter(name=name)
+ if len(existing) > 0: continue
+
+ print 'Registering property "%s" for module "%s"' % (name, plugin.name)
+
+ #if it does not exist in the db, create it
+ desc = propNode.getElementsByTagName('desc')[0]
+ if desc and len(desc.childNodes) > 0:
+ desc = desc.childNodes[0].nodeValue
+
+ property = Property(plugin_id=plugin.id, name=name, description=desc or '')
+ property.save()
+
+
+
+
+loadProps('consctrictor.properties')
+basedir = os.getcwd()
+os.chdir('constrictor_gui')
+os.environ['PYTHONPATH'] = os.path.join(os.path.abspath(''), '..')
+if not options.has_key('-u'):
+ buildDjangoDB()
+else:
+ # force a db connection so changing dirs won't affect the db
+ print "We already have the following scripts: %s" \
+ % str(tuple([s.name for s in Script.objects.all()]))
+
+os.chdir(basedir)
+updateDjangoPlugins();
+
+
--- /dev/null
+#!/usr/bin/python
+# -----------------------------------------------------------------------
+# Copyright (C) 2007-2008 King County Library System
+# Bill Erickson <erickson@esilibrary.com>
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU 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 General Public License for more details.
+# -----------------------------------------------------------------------
+
+
+'''
+This launches the Django server. This script is especially useful for
+running Django from a local (non-system-wide) install of Django
+'''
+
+import os, sys
+
+os.chdir('constrictor_gui')
+os.environ['PYTHONPATH'] = os.path.join(os.path.abspath(''), '..')
+
+if os.system('python manage.py runserver') != 0:
+ sys.stderr.write("Error starting Django admin")
--- /dev/null
+<?xml version='1.0'?>
+
+<constrictor plugin='constrictor' xmlns='http://esilibrary.com/namespaces/constrictor/v1'>
+ <desc>
+ Sample plugin
+ </desc>
+ <scripts>
+ <script name='sleep' publish='true'>
+ <desc>
+ Performs several random, sub-second sleep tests. This is a
+ good general purpose test to make sure constrictor is correctly configured.
+ </desc>
+ </script>
+ </scripts>
+</constrictor>
--- /dev/null
+#!/usr/bin/python
+# --------------------------------------------------------------
+# Simple script sample. Eacch task just sleeps for some portion
+# of a second
+# --------------------------------------------------------------
+
+
+from constrictor.task import Task
+from constrictor.script import Script, ScriptManager
+from constrictor.properties import Properties
+import random, time
+
+
+class MyTask(Task):
+ ''' Subclass the Task class, call the superclass constructor,
+ and define the run() method '''
+
+ def __init__(self, name=''):
+ Task.__init__(self, name)
+
+ def run(self):
+ # perform a random sleep
+ s = float(random.randint(0,3)) / random.randint(1,6)
+ if self.name == 'task1': s /= 2
+ time.sleep(s)
+ return self.name
+
+
+# wrap the tasks so they can be analyzed
+task = MyTask('task1').wrap()
+task2 = MyTask('task2').wrap()
+task3 = MyTask('task3').wrap()
+task4 = MyTask('task4').wrap()
+
+
+class MyScript(Script):
+ ''' Subclass the Script class, define the run() method '''
+ def run(self):
+ ret = task.run()
+ ret = task2.run()
+ ret = task3.run()
+ ret = task4.run()
+ ret = task.run()
+ ret = task2.run()
+ return True
+
+# Launch the script
+ScriptManager.go(MyScript())
+
+