Adding constrictor (per IRC conversation) source to contrib repo. needs a lot of...
authorerickson <erickson@6d9bc8c9-1ec2-4278-b937-99fde70a366f>
Wed, 26 Nov 2008 03:43:40 +0000 (03:43 +0000)
committererickson <erickson@6d9bc8c9-1ec2-4278-b937-99fde70a366f>
Wed, 26 Nov 2008 03:43:40 +0000 (03:43 +0000)
git-svn-id: svn://svn.open-ils.org/ILS-Contrib/constrictor/trunk@51 6d9bc8c9-1ec2-4278-b937-99fde70a366f

43 files changed:
COPYING [new file with mode: 0644]
LICENSE [new file with mode: 0644]
README [new file with mode: 0644]
constrictor.properties [new file with mode: 0644]
constrictor.py [new file with mode: 0755]
constrictor/__init__.py [new file with mode: 0644]
constrictor/controller.py [new file with mode: 0644]
constrictor/db.py [new file with mode: 0644]
constrictor/log.py [new file with mode: 0644]
constrictor/properties.py [new file with mode: 0644]
constrictor/script.py [new file with mode: 0644]
constrictor/task.py [new file with mode: 0644]
constrictor/utils.py [new file with mode: 0755]
constrictor_gui/__init__.py [new file with mode: 0644]
constrictor_gui/control/__init__.py [new file with mode: 0644]
constrictor_gui/control/models.py [new file with mode: 0644]
constrictor_gui/control/templates/admin/base_site.html [new file with mode: 0644]
constrictor_gui/control/templates/constrictor/.xinitrc [new file with mode: 0644]
constrictor_gui/control/templates/constrictor/actions.html [new file with mode: 0644]
constrictor_gui/control/templates/constrictor/config.html [new file with mode: 0644]
constrictor_gui/control/templates/constrictor/docs.html [new file with mode: 0644]
constrictor_gui/control/templates/constrictor/drones.html [new file with mode: 0644]
constrictor_gui/control/templates/constrictor/index.html [new file with mode: 0644]
constrictor_gui/control/templates/constrictor/props.html [new file with mode: 0644]
constrictor_gui/control/views.py [new file with mode: 0644]
constrictor_gui/manage.py [new file with mode: 0644]
constrictor_gui/settings.py [new file with mode: 0644]
constrictor_gui/urls.py [new file with mode: 0644]
contrib/evergreen/config.xml [new file with mode: 0644]
contrib/evergreen/eg_checkin.py [new file with mode: 0644]
contrib/evergreen/eg_checkout.py [new file with mode: 0644]
contrib/evergreen/eg_checkout_roundtrip.py [new file with mode: 0644]
contrib/evergreen/eg_data.py [new file with mode: 0644]
contrib/evergreen/eg_fetch_user_groups.py [new file with mode: 0644]
contrib/evergreen/eg_renew.py [new file with mode: 0644]
contrib/evergreen/eg_tasks.py [new file with mode: 0644]
contrib/evergreen/eg_title_hold.py [new file with mode: 0644]
contrib/evergreen/eg_utils.py [new file with mode: 0644]
contrib/evergreen/eg_workflow.py [new file with mode: 0644]
deploy.py [new file with mode: 0755]
runcontroller.py [new file with mode: 0755]
samples/config.xml [new file with mode: 0644]
samples/sleep.py [new file with mode: 0755]

diff --git a/COPYING b/COPYING
new file mode 100644 (file)
index 0000000..2991b96
--- /dev/null
+++ b/COPYING
@@ -0,0 +1,12 @@
+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.
+
diff --git a/LICENSE b/LICENSE
new file mode 100644 (file)
index 0000000..94a9ed0
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,674 @@
+                    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>.
diff --git a/README b/README
new file mode 100644 (file)
index 0000000..f7868c6
--- /dev/null
+++ b/README
@@ -0,0 +1,25 @@
+
+                    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
+
+
diff --git a/constrictor.properties b/constrictor.properties
new file mode 100644 (file)
index 0000000..cde8750
--- /dev/null
@@ -0,0 +1,70 @@
+# ---- 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=
diff --git a/constrictor.py b/constrictor.py
new file mode 100755 (executable)
index 0000000..d3f3fc0
--- /dev/null
@@ -0,0 +1,146 @@
+#!/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()
+
+
diff --git a/constrictor/__init__.py b/constrictor/__init__.py
new file mode 100644 (file)
index 0000000..1d60a33
--- /dev/null
@@ -0,0 +1,17 @@
+#!/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.
+# -----------------------------------------------------------------------
+
+
diff --git a/constrictor/controller.py b/constrictor/controller.py
new file mode 100644 (file)
index 0000000..2263d85
--- /dev/null
@@ -0,0 +1,359 @@
+#!/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)
+
diff --git a/constrictor/db.py b/constrictor/db.py
new file mode 100644 (file)
index 0000000..dc12969
--- /dev/null
@@ -0,0 +1,188 @@
+# -----------------------------------------------------------------------
+# 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)
+
+
+
diff --git a/constrictor/log.py b/constrictor/log.py
new file mode 100644 (file)
index 0000000..edf8c62
--- /dev/null
@@ -0,0 +1,38 @@
+#!/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
diff --git a/constrictor/properties.py b/constrictor/properties.py
new file mode 100644 (file)
index 0000000..eb7e6a5
--- /dev/null
@@ -0,0 +1,332 @@
+"""
+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'))
diff --git a/constrictor/script.py b/constrictor/script.py
new file mode 100644 (file)
index 0000000..662c70c
--- /dev/null
@@ -0,0 +1,172 @@
+#!/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)
+
+        
+
+
diff --git a/constrictor/task.py b/constrictor/task.py
new file mode 100644 (file)
index 0000000..5fbfa34
--- /dev/null
@@ -0,0 +1,88 @@
+#!/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
+
+
+
+
diff --git a/constrictor/utils.py b/constrictor/utils.py
new file mode 100755 (executable)
index 0000000..0aaaa85
--- /dev/null
@@ -0,0 +1,88 @@
+#!/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
+
+
+
+
+
diff --git a/constrictor_gui/__init__.py b/constrictor_gui/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/constrictor_gui/control/__init__.py b/constrictor_gui/control/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/constrictor_gui/control/models.py b/constrictor_gui/control/models.py
new file mode 100644 (file)
index 0000000..927d5ed
--- /dev/null
@@ -0,0 +1,52 @@
+# -----------------------------------------------------------------------
+# 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
diff --git a/constrictor_gui/control/templates/admin/base_site.html b/constrictor_gui/control/templates/admin/base_site.html
new file mode 100644 (file)
index 0000000..a739fcb
--- /dev/null
@@ -0,0 +1,15 @@
+{% 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 %}
diff --git a/constrictor_gui/control/templates/constrictor/.xinitrc b/constrictor_gui/control/templates/constrictor/.xinitrc
new file mode 100644 (file)
index 0000000..61b4652
--- /dev/null
@@ -0,0 +1 @@
+exec bery
diff --git a/constrictor_gui/control/templates/constrictor/actions.html b/constrictor_gui/control/templates/constrictor/actions.html
new file mode 100644 (file)
index 0000000..5fd0205
--- /dev/null
@@ -0,0 +1,102 @@
+<!-- =============================================================
+ 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>
+                    &nbsp;&nbsp;&middot;&nbsp;&nbsp;
+                    <a href='/control/run/'>Run Tests</a>
+                </th>
+            </tr>
+
+        </tbody>
+    </table>
+</div>
+
diff --git a/constrictor_gui/control/templates/constrictor/config.html b/constrictor_gui/control/templates/constrictor/config.html
new file mode 100644 (file)
index 0000000..b9ddc8f
--- /dev/null
@@ -0,0 +1,30 @@
+<!-- =============================================================
+ 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>
+
diff --git a/constrictor_gui/control/templates/constrictor/docs.html b/constrictor_gui/control/templates/constrictor/docs.html
new file mode 100644 (file)
index 0000000..d7ed4da
--- /dev/null
@@ -0,0 +1,117 @@
+<!-- =============================================================
+ 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 %}
+
diff --git a/constrictor_gui/control/templates/constrictor/drones.html b/constrictor_gui/control/templates/constrictor/drones.html
new file mode 100644 (file)
index 0000000..d2e9e89
--- /dev/null
@@ -0,0 +1,108 @@
+<!-- =============================================================
+ 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>
+
diff --git a/constrictor_gui/control/templates/constrictor/index.html b/constrictor_gui/control/templates/constrictor/index.html
new file mode 100644 (file)
index 0000000..5f2d269
--- /dev/null
@@ -0,0 +1,66 @@
+<!-- =============================================================
+ 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 %}
+
diff --git a/constrictor_gui/control/templates/constrictor/props.html b/constrictor_gui/control/templates/constrictor/props.html
new file mode 100644 (file)
index 0000000..56bd3cd
--- /dev/null
@@ -0,0 +1,79 @@
+<!-- =============================================================
+ 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>-&gt;&nbsp;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 %}
+-->
+
diff --git a/constrictor_gui/control/views.py b/constrictor_gui/control/views.py
new file mode 100644 (file)
index 0000000..8ce8a8c
--- /dev/null
@@ -0,0 +1,213 @@
+# -----------------------------------------------------------------------
+# 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)
+
diff --git a/constrictor_gui/manage.py b/constrictor_gui/manage.py
new file mode 100644 (file)
index 0000000..5e78ea9
--- /dev/null
@@ -0,0 +1,11 @@
+#!/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)
diff --git a/constrictor_gui/settings.py b/constrictor_gui/settings.py
new file mode 100644 (file)
index 0000000..465d699
--- /dev/null
@@ -0,0 +1,95 @@
+# 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(''), '..'))
+
diff --git a/constrictor_gui/urls.py b/constrictor_gui/urls.py
new file mode 100644 (file)
index 0000000..6f1db14
--- /dev/null
@@ -0,0 +1,17 @@
+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')),
+)
diff --git a/contrib/evergreen/config.xml b/contrib/evergreen/config.xml
new file mode 100644 (file)
index 0000000..a1534ca
--- /dev/null
@@ -0,0 +1,113 @@
+<?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>
diff --git a/contrib/evergreen/eg_checkin.py b/contrib/evergreen/eg_checkin.py
new file mode 100644 (file)
index 0000000..95ae5de
--- /dev/null
@@ -0,0 +1,22 @@
+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())
+
diff --git a/contrib/evergreen/eg_checkout.py b/contrib/evergreen/eg_checkout.py
new file mode 100644 (file)
index 0000000..fa28553
--- /dev/null
@@ -0,0 +1,32 @@
+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())
+
+
diff --git a/contrib/evergreen/eg_checkout_roundtrip.py b/contrib/evergreen/eg_checkout_roundtrip.py
new file mode 100644 (file)
index 0000000..026bf2f
--- /dev/null
@@ -0,0 +1,35 @@
+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())
+
+
diff --git a/contrib/evergreen/eg_data.py b/contrib/evergreen/eg_data.py
new file mode 100644 (file)
index 0000000..d1d0de9
--- /dev/null
@@ -0,0 +1,88 @@
+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)]
+
diff --git a/contrib/evergreen/eg_fetch_user_groups.py b/contrib/evergreen/eg_fetch_user_groups.py
new file mode 100644 (file)
index 0000000..ad31efa
--- /dev/null
@@ -0,0 +1,34 @@
+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())
+
+
+
diff --git a/contrib/evergreen/eg_renew.py b/contrib/evergreen/eg_renew.py
new file mode 100644 (file)
index 0000000..ce3c3d9
--- /dev/null
@@ -0,0 +1,29 @@
+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())
+
+
diff --git a/contrib/evergreen/eg_tasks.py b/contrib/evergreen/eg_tasks.py
new file mode 100644 (file)
index 0000000..7a79910
--- /dev/null
@@ -0,0 +1,169 @@
+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())
diff --git a/contrib/evergreen/eg_title_hold.py b/contrib/evergreen/eg_title_hold.py
new file mode 100644 (file)
index 0000000..a5a3d04
--- /dev/null
@@ -0,0 +1,47 @@
+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())
+
diff --git a/contrib/evergreen/eg_utils.py b/contrib/evergreen/eg_utils.py
new file mode 100644 (file)
index 0000000..7995a1e
--- /dev/null
@@ -0,0 +1,169 @@
+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
+
diff --git a/contrib/evergreen/eg_workflow.py b/contrib/evergreen/eg_workflow.py
new file mode 100644 (file)
index 0000000..ba51766
--- /dev/null
@@ -0,0 +1,142 @@
+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
+
+
diff --git a/deploy.py b/deploy.py
new file mode 100755 (executable)
index 0000000..d638bfc
--- /dev/null
+++ b/deploy.py
@@ -0,0 +1,166 @@
+#!/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();
+
+
diff --git a/runcontroller.py b/runcontroller.py
new file mode 100755 (executable)
index 0000000..fe5f85e
--- /dev/null
@@ -0,0 +1,29 @@
+#!/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")
diff --git a/samples/config.xml b/samples/config.xml
new file mode 100644 (file)
index 0000000..4d242f6
--- /dev/null
@@ -0,0 +1,15 @@
+<?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>
diff --git a/samples/sleep.py b/samples/sleep.py
new file mode 100755 (executable)
index 0000000..5c7382c
--- /dev/null
@@ -0,0 +1,50 @@
+#!/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())
+
+