Index: /tags/0.2/AUTHORS
===================================================================
--- /tags/0.2/AUTHORS	(revision 325)
+++ /tags/0.2/AUTHORS	(revision 325)
@@ -0,0 +1,38 @@
+MAIN DEVELOPER(S)
+=================
+
+	* Design, code and implementation
+
+	-	Ramon Bastiaans
+		< bastiaans ( a t ) sara ( d o t ) nl >
+
+ORIGINAL CONCEPT(S)
+===================
+
+	* PBS Monitor
+
+	-	Bas van der Vlies
+		< basv ( a t ) sara ( d o t ) nl >
+
+	* PBS web software
+
+	-	ROCKS Cluster group,
+		San Diego
+
+CONTRIBUTOR(S)
+==============
+
+	* Support, testing and idea's
+
+	-	Bas van der Vlies
+		< basv ( a t ) sara ( d o t ) nl >
+
+	-	Walter de Jong
+		< walter ( a t ) sara ( d o t ) nl >
+
+	-	Jaap Dijkshoorn
+		< jaap ( a t ) sara ( d o t ) nl >
+
+	* Telling me that php-mbstring is required
+
+	-	Matt Cuttler, BNL
Index: /tags/0.2/CHANGELOG
===================================================================
--- /tags/0.2/CHANGELOG	(revision 325)
+++ /tags/0.2/CHANGELOG	(revision 325)
@@ -0,0 +1,84 @@
+0.2:
+
+	web)
+		- fixed: misc. optimization and bugfixes
+		- fixed: now fully compatible with latest PHP5 and PHP4
+
+		- changed: cluster image now incorporates small text descr.
+		- changed: monarch (cluster/host) images no longer displayed
+			   for clusters that are not jobmond enabled
+		- changed: pie chart percentages are now cpu-based instead of node-based
+
+		- added: host template for Ganglia
+			 adds a extra monarch host image to Ganglia's host overview
+			 which displays/links to the jobs on that host
+			 NOTE!: be sure to copy/install new template from addons/templates
+		- added: (optional) nodes hostnames column
+			 thanks to: Daniel Barthel - daniel(d.o.t)barthel(a.t)nottingham(d.o.t)ac(d.o.t)uk
+			 for the suggestion
+
+	jobmond)
+
+		- fixed: when a job metric is longer than maximum metric length,
+			 the info is split up amongst multiple metrics
+		- fixed: no longer exit when batch server is unavailable
+			 thanks to: Peter Kruse - pk(a.t)q-leap(d.o.t)com
+			 for the patch
+		- fixed: fd closure bug causing stderr/stdout to remain open after daemonizing
+
+		- changed: rearranged code to allow support for other batch systems
+
+		- added: (experimental) SGE (Sun Grid Engine) support as batch server
+			 thanks to: Babu Sundaram - babu(a.t)cs(d.o.t)uh(d.o.t)edu
+			 who developed it for a OSCAR's Google-SoC project
+		- added: pidfile support 
+			 thanks to: Michael Jeanson - michael(a.t)ccs(d.o.t)usherbrooke(d.o.t)ca
+			 for the patch
+		- added: usage display
+			 thanks to: Michael Jeanson - michael(a.t)ccs(d.o.t)usherbrooke(d.o.t)ca
+			 for the patch
+		- added: queue selection support: ability to specify which QUEUE's to get jobinfo from
+			 thanks to: Michael Jeanson - michael(a.t)ccs(d.o.t)usherbrooke(d.o.t)ca
+			 for the patch
+
+	jobarchived)
+
+		- fixed: XML retrieval for Ganglia version >= 3.0.3 working properly again
+		- fixed: database storing for Ganglia version >= 3.0.3 working properly again
+		- fixed: fd closure bug causing stderr/stdout to remain open after daemonizing
+
+		- changed: misc. bugfixes to optimize XML connections
+		- changed: misc. bugfixes for misc. minor issues
+
+		- added: cleaning of stale jobs in dbase: see JOB_TIMEOUT option
+
+0.1.1: 
+
+	web)
+
+		- fixed: misc. layout bugs for overview & search
+		- fixed: bug that occured when calculating the number of nodes when there
+		         was more than one job running on a machine
+
+		- changed: column requested memory is now optional through conf.php
+		- changed: search and overview tables are now full screen (100%)
+		- changed: overview jobnames are now cutoff at max 9 characters
+		           to prevent (layout) scews in the tables
+		- changed: overview graphs are no longer downsized
+
+		- added: (optional) column 'queued' (since) in overview
+		- added: search results (can) now have a SEARCH_RESULT_LIMIT
+		         this increases performance of the query's significantly!
+		- added: date/time format as displayed is now configurable through conf.php
+
+	jobmond)
+
+		- added: now reports 'queued since' (or creation time) of jobs
+
+	documentation)
+
+		- fixed: wrong e-mail adress in INSTALL (doh!)
+
+0.1.0:
+
+	- First public release
Index: /tags/0.2/INSTALL
===================================================================
--- /tags/0.2/INSTALL	(revision 325)
+++ /tags/0.2/INSTALL	(revision 325)
@@ -0,0 +1,227 @@
+DESCRIPTION
+===========
+
+	Job Monarch is a set of tools to monitor and optionally archive (batch)job information.
+
+	It is a addon for the Ganglia monitoring system and plugs in to a existing Ganglia setup.
+
+	To view a operational setup with Job Monarch, have a look here: http://ganglia.sara.nl/
+
+
+	Job Monarch stands for 'Job Monitoring and Archiving' tool and consists of three (3) components:
+
+	* jobmond
+
+		The Job Monitoring Daemon.
+		  
+		Gathers PBS/Torque batch statistics on jobs/nodes and submits them into
+		Ganglia's XML stream.
+
+		Through this daemon, users are able to view the PBS/Torque batch system and the
+		jobs/nodes that are in it (be it either running or queued).
+
+	* jobarchived (optionally)
+
+		The Job Archiving Daemon.
+
+		Listens to Ganglia's XML stream and archives the job and node statistics.
+		It stores the job statistics in a Postgres SQL database and the node statistics
+		in RRD files.
+		
+		Through this daemon, users are able to lookup a old/finished job
+		and view all it's statistics.
+
+		Optionally: You can either choose to use this daemon if your users have use for it.
+		As it can be a heavy application to run and not everyone may have a need for it.
+
+		- Multithreaded:	Will not miss any data regardless of (slow) storage
+
+		- Staged writing:	Spread load over bigger time periods
+
+		- High precision RRDs:	Allow for zooming on old periods with large precision
+
+		- Timeperiod RRDs:	Allow for smaller number of files while still keeping advantage
+					of small disk space
+		
+	* web
+
+		The Job Monarch web interface.
+
+		This interfaces with the jobmond data and (optionally) the jobarchived and presents the
+		data and graphs.
+
+		It does this in a similar layout/setup as Ganglia itself, so the navigation and usage is intuitive.
+
+		- Graphical usage:	Displays graphical cluster overview so you can see the cluster (job) state
+					in one view/image and additional pie chart with relevant information on your
+					current view
+
+		- Filters:		Ability to filter output to limit information displayed (usefull for those
+					clusters with 500+ jobs). This also filters the graphical overview images output
+					and pie chart so you only see the filter relevant data
+
+		- Archive:		When enabling jobarchived, users can go back as far as recorded in the database
+					or archived RRDs to find out what happened to a crashed or old job
+
+		- Zoom ability:		Users can zoom into a timepriod as small as the smallest grain of the RRDS
+					(typically up to 10 seconds) when a jobarchived is present
+
+REQUIREMENTS
+============
+
+	all:
+
+		- Python 2.3 or higher
+
+	jobmond:
+
+		- pbs_python v2.8.2 or higher
+		  ftp://ftp.sara.nl/pub/outgoing/pbs_python.tar.gz
+
+		- gmond v3.0.1 or higher
+		  http://www.ganglia.info
+
+	jobarchived:
+
+		- Postgres SQL v7.xx
+		  http://www.postgres.org
+
+		- rrdtool v1.xx
+		  http://people.ee.ethz.ch/~oetiker/webtools/rrdtool/
+
+		- python-pgsql v4.x.x
+		  http://sourceforge.net/projects/pypgsql/
+
+		- gmetad v3.x.x
+		  http://www.ganglia.info
+
+	web:
+
+		- PHP v4.1 or higher
+		  http://www.php.net
+
+		- php-pgsql v4.x.x
+		  (should come with Postgres)
+
+		- GD v2.x
+		  http://www.boutell.com/gd/
+
+		- Ganglia web frontend v3.x.x
+		  http://www.ganglia.info
+
+
+INSTALLATION
+============
+
+	Prior to installing the software make sure you meet the necessary requirements as
+	mentioned above.
+
+	NOTE: You can choose to install to other path/directories if your setup is different.
+
+	* jobmond
+
+		1. Copy jobmond.py:
+
+		 > cp jobmond/jobmond.py /usr/local/sbin/jobmond.py
+
+		2. Copy jobmond.conf:
+		
+		 > cp jobmond/jobmond.conf /etc/jobmond.conf
+
+	* jobarchived
+
+		1. Create a Postgres SQL database for jobarchived:
+
+		 > createdb jobarchive
+
+		2. Setup jobarchived's tables:
+
+		 > psql -f jobarchived/job_dbase.sql jobarchive
+
+		3. Copy jobarchived/jobarchived.conf:
+
+		 > cp jobarchived/jobarchived.conf /etc/jobarchived.conf
+
+		4. Copy jobarchived.py and DBClass.py:
+
+		 > cp jobarchived/jobarchived.py /usr/local/sbin/jobarchived.py
+		 > cp jobarchived/DBClass.py /usr/local/sbin/DBClass.py
+
+	* web
+
+		1. Copy the Job Monarch Template to your Ganglia installation
+
+		 > cp -a web/templates/job_monarch /var/www/ganglia/templates
+
+		2. Copy the web interface files to the addon directory in Ganglia
+
+		 > cp -a web/addons/job_monarch /var/www/ganglia/addons
+
+CONFIGURATION
+=============
+
+	After installation each component requires additional configuration.
+
+	* jobmond
+	
+		1. Edit Jobmond's config to reflect your settings:
+
+		 - In /etc/jobmond.conf
+
+		   ( see config comments for syntax and explanation )
+
+	* jobarchived
+
+		1. Edit Jobarchived's config to reflect your settings:
+
+		 - In /etc/jobarchived.conf
+
+		   ( see config comments for syntax and explanation )
+
+	* web
+
+		1. Change your Ganglia's web template to Job Monarch
+
+		 - In /var/www/ganglia/conf.php:
+
+		 > $template_name = "job_monarch";
+
+		2. Change Job Monarch's config to reflect your settings:
+
+		 - In /var/www/ganglia/addons/job_monarch/conf.php
+
+		   ( see config comments for syntax and explanation )
+
+START
+=====
+
+	* jobmond
+
+		The Job Monitor has to be run on a machine that is allowed to
+		query the PBS/Torque server.
+		Make sure that if you have 'acl_hosts' enabled on your PBS/Torque
+		server that jobmond's machine is in it.
+
+		1. Start the Job Monitor:
+
+		 > /usr/local/sbin/jobmond.py -c /etc/jobmond.conf
+
+	* jobarchived
+
+		1. Start the Job Archiver:
+
+		 > /usr/local/sbin/jobarchived.py -c /etc/jobarchived.conf
+
+	* web
+
+		Doesn't require you to (re)start anything.
+		( make sure the Postgres database is running though )
+
+CONTACT
+=======
+
+	To contact the author for anything from bugfixes to flame/hate mail:
+
+	* Ramon Bastiaans
+
+	  <bastiaans ( a t ) sara ( d o t ) nl>
Index: /tags/0.2/LICENSE
===================================================================
--- /tags/0.2/LICENSE	(revision 325)
+++ /tags/0.2/LICENSE	(revision 325)
@@ -0,0 +1,345 @@
+		    GNU GENERAL PUBLIC LICENSE
+		       Version 2, June 1991
+
+ Copyright (C) 1989, 1991 Free Software Foundation, Inc.
+                       51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+			    Preamble
+
+  The licenses for most software are designed to take away your
+freedom to share and change it.  By contrast, the GNU General Public
+License is intended to guarantee your freedom to share and change free
+software--to make sure the software is free for all its users.  This
+General Public License applies to most of the Free Software
+Foundation's software and to any other program whose authors commit to
+using it.  (Some other Free Software Foundation software is covered by
+the GNU Library General Public License instead.)  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
+this service 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 make restrictions that forbid
+anyone to deny you these rights or to ask you to surrender the rights.
+These restrictions translate to certain responsibilities for you if you
+distribute copies of the software, or if you modify it.
+
+  For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must give the recipients all the rights that
+you have.  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.
+
+  We protect your rights with two steps: (1) copyright the software, and
+(2) offer you this license which gives you legal permission to copy,
+distribute and/or modify the software.
+
+  Also, for each author's protection and ours, we want to make certain
+that everyone understands that there is no warranty for this free
+software.  If the software is modified by someone else and passed on, we
+want its recipients to know that what they have is not the original, so
+that any problems introduced by others will not reflect on the original
+authors' reputations.
+
+  Finally, any free program is threatened constantly by software
+patents.  We wish to avoid the danger that redistributors of a free
+program will individually obtain patent licenses, in effect making the
+program proprietary.  To prevent this, we have made it clear that any
+patent must be licensed for everyone's free use or not licensed at all.
+
+  The precise terms and conditions for copying, distribution and
+modification follow.
+
+
+		    GNU GENERAL PUBLIC LICENSE
+   TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+  0. This License applies to any program or other work which contains
+a notice placed by the copyright holder saying it may be distributed
+under the terms of this General Public License.  The "Program", below,
+refers to any such program or work, and a "work based on the Program"
+means either the Program or any derivative work under copyright law:
+that is to say, a work containing the Program or a portion of it,
+either verbatim or with modifications and/or translated into another
+language.  (Hereinafter, translation is included without limitation in
+the term "modification".)  Each licensee is addressed as "you".
+
+Activities other than copying, distribution and modification are not
+covered by this License; they are outside its scope.  The act of
+running the Program is not restricted, and the output from the Program
+is covered only if its contents constitute a work based on the
+Program (independent of having been made by running the Program).
+Whether that is true depends on what the Program does.
+
+  1. You may copy and distribute 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 and disclaimer of warranty; keep intact all the
+notices that refer to this License and to the absence of any warranty;
+and give any other recipients of the Program a copy of this License
+along with the Program.
+
+You may charge a fee for the physical act of transferring a copy, and
+you may at your option offer warranty protection in exchange for a fee.
+
+  2. You may modify your copy or copies of the Program or any portion
+of it, thus forming a work based on the Program, and copy and
+distribute such modifications or work under the terms of Section 1
+above, provided that you also meet all of these conditions:
+
+    a) You must cause the modified files to carry prominent notices
+    stating that you changed the files and the date of any change.
+
+    b) You must cause any work that you distribute or publish, that in
+    whole or in part contains or is derived from the Program or any
+    part thereof, to be licensed as a whole at no charge to all third
+    parties under the terms of this License.
+
+    c) If the modified program normally reads commands interactively
+    when run, you must cause it, when started running for such
+    interactive use in the most ordinary way, to print or display an
+    announcement including an appropriate copyright notice and a
+    notice that there is no warranty (or else, saying that you provide
+    a warranty) and that users may redistribute the program under
+    these conditions, and telling the user how to view a copy of this
+    License.  (Exception: if the Program itself is interactive but
+    does not normally print such an announcement, your work based on
+    the Program is not required to print an announcement.)
+
+
+These requirements apply to the modified work as a whole.  If
+identifiable sections of that work are not derived from the Program,
+and can be reasonably considered independent and separate works in
+themselves, then this License, and its terms, do not apply to those
+sections when you distribute them as separate works.  But when you
+distribute the same sections as part of a whole which is a work based
+on the Program, the distribution of the whole must be on the terms of
+this License, whose permissions for other licensees extend to the
+entire whole, and thus to each and every part regardless of who wrote it.
+
+Thus, it is not the intent of this section to claim rights or contest
+your rights to work written entirely by you; rather, the intent is to
+exercise the right to control the distribution of derivative or
+collective works based on the Program.
+
+In addition, mere aggregation of another work not based on the Program
+with the Program (or with a work based on the Program) on a volume of
+a storage or distribution medium does not bring the other work under
+the scope of this License.
+
+  3. You may copy and distribute the Program (or a work based on it,
+under Section 2) in object code or executable form under the terms of
+Sections 1 and 2 above provided that you also do one of the following:
+
+    a) Accompany it with the complete corresponding machine-readable
+    source code, which must be distributed under the terms of Sections
+    1 and 2 above on a medium customarily used for software interchange; or,
+
+    b) Accompany it with a written offer, valid for at least three
+    years, to give any third party, for a charge no more than your
+    cost of physically performing source distribution, a complete
+    machine-readable copy of the corresponding source code, to be
+    distributed under the terms of Sections 1 and 2 above on a medium
+    customarily used for software interchange; or,
+
+    c) Accompany it with the information you received as to the offer
+    to distribute corresponding source code.  (This alternative is
+    allowed only for noncommercial distribution and only if you
+    received the program in object code or executable form with such
+    an offer, in accord with Subsection b above.)
+
+The source code for a work means the preferred form of the work for
+making modifications to it.  For an executable work, complete source
+code means all the source code for all modules it contains, plus any
+associated interface definition files, plus the scripts used to
+control compilation and installation of the executable.  However, as a
+special exception, the source code distributed need not include
+anything that is normally distributed (in either source or binary
+form) with the major components (compiler, kernel, and so on) of the
+operating system on which the executable runs, unless that component
+itself accompanies the executable.
+
+If distribution of executable or object code is made by offering
+access to copy from a designated place, then offering equivalent
+access to copy the source code from the same place counts as
+distribution of the source code, even though third parties are not
+compelled to copy the source along with the object code.
+
+
+  4. You may not copy, modify, sublicense, or distribute the Program
+except as expressly provided under this License.  Any attempt
+otherwise to copy, modify, sublicense or distribute the Program is
+void, and will automatically terminate your rights under this License.
+However, parties who have received copies, or rights, from you under
+this License will not have their licenses terminated so long as such
+parties remain in full compliance.
+
+  5. You are not required to accept this License, since you have not
+signed it.  However, nothing else grants you permission to modify or
+distribute the Program or its derivative works.  These actions are
+prohibited by law if you do not accept this License.  Therefore, by
+modifying or distributing the Program (or any work based on the
+Program), you indicate your acceptance of this License to do so, and
+all its terms and conditions for copying, distributing or modifying
+the Program or works based on it.
+
+  6. Each time you redistribute the Program (or any work based on the
+Program), the recipient automatically receives a license from the
+original licensor to copy, distribute or modify the Program subject to
+these terms and conditions.  You may not impose any further
+restrictions on the recipients' exercise of the rights granted herein.
+You are not responsible for enforcing compliance by third parties to
+this License.
+
+  7. If, as a consequence of a court judgment or allegation of patent
+infringement or for any other reason (not limited to patent issues),
+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
+distribute so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you
+may not distribute the Program at all.  For example, if a patent
+license would not permit royalty-free redistribution of the Program by
+all those who receive copies directly or indirectly through you, then
+the only way you could satisfy both it and this License would be to
+refrain entirely from distribution of the Program.
+
+If any portion of this section is held invalid or unenforceable under
+any particular circumstance, the balance of the section is intended to
+apply and the section as a whole is intended to apply in other
+circumstances.
+
+It is not the purpose of this section to induce you to infringe any
+patents or other property right claims or to contest validity of any
+such claims; this section has the sole purpose of protecting the
+integrity of the free software distribution system, which is
+implemented by public license practices.  Many people have made
+generous contributions to the wide range of software distributed
+through that system in reliance on consistent application of that
+system; it is up to the author/donor to decide if he or she is willing
+to distribute software through any other system and a licensee cannot
+impose that choice.
+
+This section is intended to make thoroughly clear what is believed to
+be a consequence of the rest of this License.
+
+
+  8. If the distribution and/or use of the Program is restricted in
+certain countries either by patents or by copyrighted interfaces, the
+original copyright holder who places the Program under this License
+may add an explicit geographical distribution limitation excluding
+those countries, so that distribution is permitted only in or among
+countries not thus excluded.  In such case, this License incorporates
+the limitation as if written in the body of this License.
+
+  9. The Free Software Foundation may publish revised and/or new versions
+of the 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 a version number of this License which applies to it and "any
+later version", you have the option of following the terms and conditions
+either of that version or of any later version published by the Free
+Software Foundation.  If the Program does not specify a version number of
+this License, you may choose any version ever published by the Free Software
+Foundation.
+
+  10. If you wish to incorporate parts of the Program into other free
+programs whose distribution conditions are different, write to the author
+to ask for permission.  For software which is copyrighted by the Free
+Software Foundation, write to the Free Software Foundation; we sometimes
+make exceptions for this.  Our decision will be guided by the two goals
+of preserving the free status of all derivatives of our free software and
+of promoting the sharing and reuse of software generally.
+
+			    NO WARRANTY
+
+  11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, 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.
+
+  12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
+REDISTRIBUTE 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.
+
+		     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
+convey 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 2 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, write to the Free Software
+    Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
+
+
+Also add information on how to contact you by electronic and paper mail.
+
+If the program is interactive, make it output a short notice like this
+when it starts in an interactive mode:
+
+    Gnomovision version 69, Copyright (C) year name of author
+    Gnomovision 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, the commands you use may
+be called something other than `show w' and `show c'; they could even be
+mouse-clicks or menu items--whatever suits your program.
+
+You should also get your employer (if you work as a programmer) or your
+school, if any, to sign a "copyright disclaimer" for the program, if
+necessary.  Here is a sample; alter the names:
+
+  Yoyodyne, Inc., hereby disclaims all copyright interest in the program
+  `Gnomovision' (which makes passes at compilers) written by James Hacker.
+
+  <signature of Ty Coon>, 1 April 1989
+  Ty Coon, President of Vice
+
+This 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 Library General
+Public License instead of this License.
Index: /tags/0.2/TODO
===================================================================
--- /tags/0.2/TODO	(revision 325)
+++ /tags/0.2/TODO	(revision 325)
@@ -0,0 +1,27 @@
+Functionality TODO (wish) list
+==============================
+
+	*	Improved config file and arguments usage for daemons
+		(also --help and possibly a man page)
+
+	*	Actual version reporting by daemons
+
+	*	Usage of a node's (Ganglia) location in the ClusterImage
+		construction (placement by x,y,z coordinates)
+  
+	*	More advanced and detailed graph's, etc
+
+	*	Clickable nodes in the "big" cluster image,
+		filtering all jobs on that particular node.
+
+	*	Mouse hints on the "big" cluster image,
+		showing node information detail when holding
+		over a node. Information such as jobid(s),
+		queues, users, etc
+
+	*	Usage of regexp's or wildcards at jobsearch
+		i.e. for job name/user/queue
+
+	*	Ability to search by certain node properties
+		instead of job properties. I.e.: a certain node
+		hostname and/or number of nodes/cpus
Index: /tags/0.2/jobarchived/DBClass.py
===================================================================
--- /tags/0.2/jobarchived/DBClass.py	(revision 325)
+++ /tags/0.2/jobarchived/DBClass.py	(revision 325)
@@ -0,0 +1,200 @@
+#!/usr/bin/env python
+#
+# Orginal from Andre van der Vlies <andre@vandervlies.xs4all.nl> for MySQL. Changed
+# and added some more functions for postgres.
+#
+#
+# Changed by: Bas van der Vlies <basv@sara.nl>
+#
+# SARA API for Postgres Database
+#
+# Changed by: Ramon Bastiaans for Job Monarch
+#
+#
+# This file is part of Jobmonarch
+#
+# Copyright (C) 2006  Ramon Bastiaans
+#
+# Jobmonarch 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 2 of the License, or
+# (at your option) any later version.
+#
+# Jobmonarch 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, write to the Free Software
+# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
+#
+# SVN $Id$
+#
+"""
+This is an generic SARA python class that connects to any postgres database.
+THe default one is uva_cluster_db, but that can specified as argument.
+
+The basic usage is:
+	import DBClass
+
+	db_vars = DBClass.InitVars(DataBaseName='rc_cluster_db',
+				User='root',
+				Host='localhost',
+				Password='',
+				Dictionary='true')
+	db = DBClass.DB(db_vars)
+
+	# Get valuese from database
+	print db.Get("SELECT * FROM clusters")
+
+	# Set values into database
+	bla=db.Set("DELETE FROM clusters WHERE clustername = 'bas'")
+"""
+from pyPgSQL import PgSQL
+
+#
+# Class to 'simplify' the declaration of 'global'
+# variables....
+#
+class InitVars:
+	Vars = {}
+
+	def __init__(self, **key_arg):
+		for (key, value) in key_arg.items():
+			if value:
+				self.Vars[key] = value
+			else:	
+				self.Vars[key] = None
+
+	def __call__(self, *key):
+		key = "%s" % key
+		return self.Vars[key]
+
+	def __getitem__(self, key):
+		return self.Vars[key]
+
+	def __repr__(self):
+		return repr(self.Vars)
+
+	def keys(self):
+		barf =  map(None, self.Vars.keys())
+		return barf
+
+	def values(self):
+		barf =  map(None, self.Vars.values())
+		return barf
+	
+	def has_key(self, key):
+		if self.Vars.has_key(key):
+			return 1
+		else:
+			return 0
+
+class DBError(Exception):
+	def __init__(self, msg=''):
+		self.msg = msg
+		Exception.__init__(self, msg)
+	def __repr__(self):
+		return self.msg
+	__str__ = __repr__
+
+#
+# Class to connect to a database
+# and return the queury in a list or dictionairy.
+#
+class DB:
+	def __init__(self, db_vars):
+
+		self.dict = db_vars
+
+		if self.dict.has_key('User'):
+			self.user = self.dict['User']
+		else:
+			self.user = 'postgres'
+
+		if self.dict.has_key('Host'):
+			self.host = self.dict['Host']
+		else:
+			self.host = 'localhost'
+
+		if self.dict.has_key('Password'):
+			self.passwd = self.dict['Password']
+		else:
+			self.passwd = ''
+
+		if self.dict.has_key('DataBaseName'):
+			self.db = self.dict['DataBaseName']
+		else:
+			self.db = 'uva_cluster_db'
+
+		# connect_string = 'host:port:database:user:password:
+		dsn = "%s::%s:%s:%s" %(self.host, self.db, self.user, self.passwd)
+
+		try:
+			self.SQL = PgSQL.connect(dsn)
+		except PgSQL.Error, details:
+			str = "%s" %details
+			raise DBError(str)
+
+	def __repr__(self):
+		return repr(self.result)
+
+	def __nonzero__(self):
+		return not(self.result == None)
+
+	def __len__(self):
+		return len(self.result)
+
+	def __getitem__(self,i):
+		return self.result[i]
+
+	def __getslice__(self,i,j):
+		return self.result[i:j]
+
+	def Get(self, q_str):
+		c = self.SQL.cursor()
+		try:
+			c.execute(q_str)
+			result = c.fetchall()
+		except PgSQL.Error, details:
+			c.close()
+			str = "%s" %details
+			raise DBError(str)
+
+		c.close()
+		return result
+
+	def Set(self, q_str):
+		c = self.SQL.cursor()
+		try:
+			c.execute(q_str)
+		        result = c.oidValue
+
+		except PgSQL.Error, details:
+			c.close()
+			str = "%s" %details
+			raise DBError(str)
+
+		c.close()
+		return result
+
+	def Commit(self):
+		self.SQL.commit()
+
+#
+# Some tests....
+#
+def main():
+	db_vars = InitVars(DataBaseName='rc_cluster_db',
+				User='root',
+				Host='localhost',
+				Password='',
+				Dictionary='true')
+	print db_vars;
+
+	db = DB(db_vars)
+	print db.Get("""SELECT * FROM clusters""")
+
+if __name__ == '__main__':
+	main()
Index: /tags/0.2/jobarchived/examples/jobarchived.conf
===================================================================
--- /tags/0.2/jobarchived/examples/jobarchived.conf	(revision 325)
+++ /tags/0.2/jobarchived/examples/jobarchived.conf	(revision 325)
@@ -0,0 +1,13 @@
+[DEFAULT]
+DAEMONIZE			: 1
+DEBUG_LEVEL			: 1
+USE_SYSLOG			: 1
+SYSLOG_LEVEL			: 0
+SYSLOG_FACILITY			: DAEMON
+GMETAD_CONF			: /etc/gmetad.conf
+ARCHIVE_XMLSOURCE		: localhost:8651
+ARCHIVE_DATASOURCES		: "LISA Cluster"
+ARCHIVE_HOURS_PER_RRD		: 12
+ARCHIVE_EXCLUDE_METRICS		: "^Temp_.*_.*", ".*_Temp_.*", ".*_RPM_.*", ".*_Battery_.*"
+ARCHIVE_PATH			: /data/jobarch/rrds
+JOB_SQL_DBASE			: localhost/jobarch
Index: /tags/0.2/jobarchived/job_dbase.sql
===================================================================
--- /tags/0.2/jobarchived/job_dbase.sql	(revision 325)
+++ /tags/0.2/jobarchived/job_dbase.sql	(revision 325)
@@ -0,0 +1,34 @@
+DROP TABLE jobs CASCADE;
+DROP TABLE nodes CASCADE;
+DROP TABLE job_nodes CASCADE;
+
+CREATE TABLE jobs (
+	job_id			INT NOT NULL UNIQUE PRIMARY KEY,
+	job_name		VARCHAR(100),
+	job_queue		VARCHAR(50),
+	job_owner		VARCHAR(30),
+	job_requested_time	VARCHAR(10),
+	job_requested_memory	VARCHAR(10),
+	job_ppn			INT,
+	job_status		VARCHAR(2),
+	job_start_timestamp	VARCHAR(10),
+	job_stop_timestamp	VARCHAR(10)
+);
+
+CREATE TABLE nodes (
+	node_id			SERIAL NOT NULL UNIQUE PRIMARY KEY,
+	node_hostname		VARCHAR(100),
+	node_location		VARCHAR(10)
+);
+
+CREATE TABLE job_nodes (
+	job_id			INT NOT NULL REFERENCES jobs ON DELETE CASCADE,
+	node_id			INT NOT NULL REFERENCES nodes ON DELETE RESTRICT,
+	PRIMARY KEY ( job_id, node_id )
+);
+
+CREATE USER root;
+CREATE USER "www-data";
+
+GRANT ALL ON jobs,nodes,job_nodes TO root,"www-data";
+GRANT ALL ON nodes_node_id_seq TO root,"www-data";
Index: /tags/0.2/jobarchived/jobarchived.conf-dist
===================================================================
--- /tags/0.2/jobarchived/jobarchived.conf-dist	(revision 325)
+++ /tags/0.2/jobarchived/jobarchived.conf-dist	(revision 325)
@@ -0,0 +1,88 @@
+[DEFAULT]
+# Wether or not to run as a daemon in background
+#
+DAEMONIZE			: 1
+
+# Specify debugging level here (only when _not_ DAEMONIZE)
+#
+# 11 = XML: metrics
+# 10 = XML: host, cluster, grid, ganglia
+# 9  = RRD activity, gmetad config parsing
+# 8  = RRD file activity
+# 6  = SQL
+# 1  = daemon threading
+# 0  = errors
+#
+# default: 0
+#
+DEBUG_LEVEL			: 0
+
+# Enable logging to syslog?
+#
+USE_SYSLOG			: 1
+
+# What level msg'es should be logged to syslog?
+#
+# usually: lvl 0 (errors)
+#
+SYSLOG_LEVEL			: 0
+
+# Which facility to use in syslog
+#
+# Known:
+#       KERN, USER, MAIL, DAEMON, AUTH, LPR,
+#       NEWS, UUCP, CRON and LOCAL0 through LOCAL7
+#
+SYSLOG_FACILITY			: DAEMON
+
+# Where is the gmetad.conf located
+#
+GMETAD_CONF			: /etc/gmetad.conf
+
+# Where to grab XML data from
+# Usually: local gmetad (port 8651)
+#
+# Syntax: <hostname>:<port>
+#
+ARCHIVE_XMLSOURCE		: localhost:8651
+
+# List of data_source names to archive for
+#
+# Syntax: [ "<clustername>", "<clustername>" ]
+#
+ARCHIVE_DATASOURCES		: "My Cluster"
+
+# Amount of hours to store in one single archived rrd
+#
+# If you would like less files you can set this bigger
+# but could degrade performance
+#
+# For now 12 hours seems to work: 2 periods per day
+#
+ARCHIVE_HOURS_PER_RRD		: 12
+
+# Which metrics to exclude from archiving
+# NOTE: This can be a regexp or a string
+#
+ARCHIVE_EXCLUDE_METRICS		: ".*Temp.*", ".*RPM.*", ".*Version.*", ".*Tag$", "boottime", "gexec", "os.*", "machine_type"
+
+# Where to store the archived rrd's
+#
+ARCHIVE_PATH			: /var/lib/jobarch
+
+# Archive's SQL dbase to use
+#
+# Syntax: <hostname>/<database>
+#
+JOB_SQL_DBASE			: localhost/jobarchive
+
+# Timeout for jobs in archive
+#
+# Assume job has already finished while jobarchived was not running
+# after this amount of hours: the it will be finished anyway in the database
+#
+JOB_TIMEOUT			: 168
+
+# Location of rrdtool binary
+#
+RRDTOOL				: /usr/bin/rrdtool
Index: /tags/0.2/jobarchived/jobarchived.py
===================================================================
--- /tags/0.2/jobarchived/jobarchived.py	(revision 325)
+++ /tags/0.2/jobarchived/jobarchived.py	(revision 325)
@@ -0,0 +1,1709 @@
+#!/usr/bin/env python
+#
+# This file is part of Jobmonarch
+#
+# Copyright (C) 2006  Ramon Bastiaans
+#
+# Jobmonarch 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 2 of the License, or
+# (at your option) any later version.
+#
+# Jobmonarch 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, write to the Free Software
+# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
+#
+# SVN $Id$
+#
+
+DEFAULT_SEARCH_PATH     = '/usr/share/jobarchived'
+
+import sys
+
+if DEFAULT_SEARCH_PATH not in sys.path:
+
+        sys.path.append( DEFAULT_SEARCH_PATH )
+
+import getopt, syslog, ConfigParser
+
+def processArgs( args ):
+
+        SHORT_L	= 'c:'
+        LONG_L	= 'config='
+
+        config_filename = None
+
+        try:
+
+                opts, args = getopt.getopt( args, SHORT_L, LONG_L )
+
+        except getopt.error, detail:
+
+                print detail
+                sys.exit(1)
+
+        for opt, value in opts:
+
+                if opt in [ '--config', '-c' ]:
+
+                        config_filename = value
+
+        if not config_filename:
+
+                config_filename = '/etc/jobarchived.conf'
+
+	try:
+		return loadConfig( config_filename )
+
+	except ConfigParser.NoOptionError, detail:
+
+		print detail
+		sys.exit( 1 )
+
+def loadConfig( filename ):
+
+	def getlist( cfg_string ):
+
+		my_list = [ ]
+
+		for item_txt in cfg_string.split( ',' ):
+
+			sep_char = None
+
+			item_txt = item_txt.strip()
+
+			for s_char in [ "'", '"' ]:
+
+				if item_txt.find( s_char ) != -1:
+
+					if item_txt.count( s_char ) != 2:
+
+						print 'Missing quote: %s' %item_txt
+						sys.exit( 1 )
+
+					else:
+
+						sep_char = s_char
+						break
+
+			if sep_char:
+
+				item_txt = item_txt.split( sep_char )[1]
+
+			my_list.append( item_txt )
+
+		return my_list
+
+	cfg = ConfigParser.ConfigParser()
+
+	cfg.read( filename )
+
+	global DEBUG_LEVEL, USE_SYSLOG, SYSLOG_LEVEL, SYSLOG_FACILITY, GMETAD_CONF, ARCHIVE_XMLSOURCE, ARCHIVE_DATASOURCES, ARCHIVE_PATH, ARCHIVE_HOURS_PER_RRD, ARCHIVE_EXCLUDE_METRICS, JOB_SQL_DBASE, DAEMONIZE, RRDTOOL, JOB_TIMEOUT
+
+	ARCHIVE_PATH		= cfg.get( 'DEFAULT', 'ARCHIVE_PATH' )
+
+	ARCHIVE_HOURS_PER_RRD	= cfg.getint( 'DEFAULT', 'ARCHIVE_HOURS_PER_RRD' )
+
+	DEBUG_LEVEL		= cfg.getint( 'DEFAULT', 'DEBUG_LEVEL' )
+
+	USE_SYSLOG		= cfg.getboolean( 'DEFAULT', 'USE_SYSLOG' )
+
+	SYSLOG_LEVEL		= cfg.getint( 'DEFAULT', 'SYSLOG_LEVEL' )
+
+	try:
+
+		SYSLOG_FACILITY	= eval( 'syslog.LOG_' + cfg.get( 'DEFAULT', 'SYSLOG_FACILITY' ) )
+
+	except AttributeError, detail:
+
+		print 'Unknown syslog facility'
+		sys.exit( 1 )
+
+	GMETAD_CONF		= cfg.get( 'DEFAULT', 'GMETAD_CONF' )
+
+	ARCHIVE_XMLSOURCE	= cfg.get( 'DEFAULT', 'ARCHIVE_XMLSOURCE' )
+
+        ARCHIVE_DATASOURCES	= getlist( cfg.get( 'DEFAULT', 'ARCHIVE_DATASOURCES' ) )
+
+	ARCHIVE_EXCLUDE_METRICS	= getlist( cfg.get( 'DEFAULT', 'ARCHIVE_EXCLUDE_METRICS' ) )
+
+	JOB_SQL_DBASE		= cfg.get( 'DEFAULT', 'JOB_SQL_DBASE' )
+
+	JOB_TIMEOUT		= cfg.getint( 'DEFAULT', 'JOB_TIMEOUT' )
+
+	DAEMONIZE		= cfg.getboolean( 'DEFAULT', 'DAEMONIZE' )
+
+	RRDTOOL			= cfg.get( 'DEFAULT', 'RRDTOOL' )
+
+	return True
+
+# What XML data types not to store
+#
+UNSUPPORTED_ARCHIVE_TYPES = [ 'string' ]
+
+# Maximum time (in seconds) a parsethread may run
+#
+PARSE_TIMEOUT = 60
+
+# Maximum time (in seconds) a storethread may run
+#
+STORE_TIMEOUT = 360
+
+"""
+The Job Archiving Daemon
+"""
+
+from types import *
+
+import DBClass
+import xml.sax, xml.sax.handler, socket, string, os, os.path, time, thread, threading, random, re
+
+class DataSQLStore:
+
+	db_vars = None
+	dbc = None
+
+	def __init__( self, hostname, database ):
+
+		self.db_vars = DBClass.InitVars(DataBaseName=database,
+				User='root',
+				Host=hostname,
+				Password='',
+				Dictionary='true')
+
+		try:
+			self.dbc     = DBClass.DB(self.db_vars)
+		except DBClass.DBError, details:
+			debug_msg( 0, 'FATAL ERROR: Unable to connect to database!: ' +str(details) )
+			sys.exit(1)
+
+	def setDatabase(self, statement):
+		ret = self.doDatabase('set', statement)
+		return ret
+		
+	def getDatabase(self, statement):
+		ret = self.doDatabase('get', statement)
+		return ret
+
+	def doDatabase(self, type, statement):
+
+		debug_msg( 6, 'doDatabase(): %s: %s' %(type, statement) )
+		try:
+			if type == 'set':
+				result = self.dbc.Set( statement )
+				self.dbc.Commit()
+			elif type == 'get':
+				result = self.dbc.Get( statement )
+				
+		except DBClass.DBError, detail:
+			operation = statement.split(' ')[0]
+			debug_msg( 0, 'FATAL ERROR: ' +operation+ ' on database failed while doing ['+statement+'] full msg: '+str(detail) )
+			sys.exit(1)
+
+		debug_msg( 6, 'doDatabase(): result: %s' %(result) )
+		return result
+
+	def getJobNodeId( self, job_id, node_id ):
+
+		id = self.getDatabase( "SELECT job_id,node_id FROM job_nodes WHERE job_id = '%s' AND node_id = '%s'" %(job_id, node_id) )
+		if len( id ) > 0:
+
+			if len( id[0] ) > 0 and id[0] != '':
+			
+				return 1
+
+		return 0
+
+	def getNodeId( self, hostname ):
+
+		id = self.getDatabase( "SELECT node_id FROM nodes WHERE node_hostname = '%s'" %hostname )
+
+		if len( id ) > 0:
+
+			id = id[0][0]
+
+			return id
+		else:
+			return None
+
+	def getNodeIds( self, hostnames ):
+
+		ids = [ ]
+
+		for node in hostnames:
+
+			id = self.getNodeId( node )
+
+			if id:
+				ids.append( id )
+
+		return ids
+
+	def getJobId( self, jobid ):
+
+		id = self.getDatabase( "SELECT job_id FROM jobs WHERE job_id = '%s'" %jobid )
+
+		if id:
+			id = id[0][0]
+
+			return id
+		else:
+			return None
+
+	def addJob( self, job_id, jobattrs ):
+
+		if not self.getJobId( job_id ):
+
+			self.mutateJob( 'insert', job_id, jobattrs ) 
+		else:
+			self.mutateJob( 'update', job_id, jobattrs )
+
+	def mutateJob( self, action, job_id, jobattrs ):
+
+		job_values	= [ 'name', 'queue', 'owner', 'requested_time', 'requested_memory', 'ppn', 'status', 'start_timestamp', 'stop_timestamp' ]
+
+		insert_col_str	= 'job_id'
+		insert_val_str	= "'%s'" %job_id
+		update_str	= None
+
+		debug_msg( 6, 'mutateJob(): %s %s' %(action,job_id))
+
+		ids = [ ]
+
+		for valname, value in jobattrs.items():
+
+			if valname in job_values and value != '':
+
+				column_name = 'job_' + valname
+
+				if action == 'insert':
+
+					if not insert_col_str:
+						insert_col_str = column_name
+					else:
+						insert_col_str = insert_col_str + ',' + column_name
+
+					if not insert_val_str:
+						insert_val_str = value
+					else:
+						insert_val_str = insert_val_str + ",'%s'" %value
+
+				elif action == 'update':
+					
+					if not update_str:
+						update_str = "%s='%s'" %(column_name, value)
+					else:
+						update_str = update_str + ",%s='%s'" %(column_name, value)
+
+			elif valname == 'nodes' and value:
+
+				node_valid = 1
+
+				if len(value) == 1:
+				
+					if jobattrs['status'] == 'Q':
+
+						node_valid = 0
+
+					else:
+
+						node_valid = 0
+
+						for node_char in str(value[0]):
+
+							if string.find( string.digits, node_char ) != -1 and not node_valid:
+
+								node_valid = 1
+
+				if node_valid:
+
+					ids = self.addNodes( value, jobattrs['domain'] )
+
+		if action == 'insert':
+
+			self.setDatabase( "INSERT INTO jobs ( %s ) VALUES ( %s )" %( insert_col_str, insert_val_str ) )
+
+		elif action == 'update':
+
+			self.setDatabase( "UPDATE jobs SET %s WHERE job_id=%s" %(update_str, job_id) )
+
+		if len( ids ) > 0:
+			self.addJobNodes( job_id, ids )
+
+	def addNodes( self, hostnames, domain ):
+
+		ids = [ ]
+
+		for node in hostnames:
+
+			node	= '%s.%s' %( node, domain )
+			id	= self.getNodeId( node )
+	
+			if not id:
+				self.setDatabase( "INSERT INTO nodes ( node_hostname ) VALUES ( '%s' )" %node )
+				id = self.getNodeId( node )
+
+			ids.append( id )
+
+		return ids
+
+	def addJobNodes( self, jobid, nodes ):
+
+		for node in nodes:
+
+			if not self.getJobNodeId( jobid, node ):
+
+				self.addJobNode( jobid, node )
+
+	def addJobNode( self, jobid, nodeid ):
+
+		self.setDatabase( "INSERT INTO job_nodes (job_id,node_id) VALUES ( %s,%s )" %(jobid, nodeid) )
+
+	def storeJobInfo( self, jobid, jobattrs ):
+
+		self.addJob( jobid, jobattrs )
+
+	def checkStaleJobs( self ):
+
+		q = "SELECT * from jobs WHERE job_status != 'F'"
+
+		r = self.getDatabase( q )
+
+		if len( r ) == 0:
+
+			return None
+
+		cleanjobs	= [ ]
+		timeoutjobs	= [ ]
+
+		jobtimeout_sec	= JOB_TIMEOUT * (60 * 60)
+		cur_time	= time.time()
+
+		for row in r:
+
+			job_id			= row[0]
+			job_requested_time	= row[4]
+			job_status		= row[7]
+			job_start_timestamp	= row[8]
+
+			if job_status == 'Q' or not job_start_timestamp:
+
+				cleanjobs.append( job_id )
+
+			else:
+
+				start_timestamp = int( job_start_timestamp )
+
+				if ( cur_time - start_timestamp ) > jobtimeout_sec:
+
+					if job_requested_time:
+
+						rtime_epoch	= reqtime2epoch( job_requested_time )
+					else:
+						rtime_epoch	= None
+					
+					timeoutjobs.append( (job_id, job_start_timestamp, rtime_epoch) )
+
+		debug_msg( 1, 'Found ' + str( len( cleanjobs ) ) + ' stale jobs in database: deleting entries' )
+
+		for j in cleanjobs:
+
+			q = "DELETE FROM jobs WHERE job_id = '" + str( j ) + "'"
+			self.setDatabase( q )
+
+		debug_msg( 1, 'Found ' + str( len( timeoutjobs ) ) + ' timed out jobs in database: closing entries' )
+
+		for j in timeoutjobs:
+
+			( i, s, r )		= j
+
+			if r:
+				new_end_timestamp	= int( s ) + r
+
+			q = "UPDATE jobs SET job_stop_timestamp = '" + str( new_end_timestamp ) + "' WHERE job_id = '" + str(i) + "'"
+			self.setDatabase( q )
+
+class RRDMutator:
+	"""A class for performing RRD mutations"""
+
+	binary = None
+
+	def __init__( self, binary=None ):
+		"""Set alternate binary if supplied"""
+
+		if binary:
+			self.binary = binary
+
+	def create( self, filename, args ):
+		"""Create a new rrd with args"""
+
+		return self.perform( 'create', '"' + filename + '"', args )
+
+	def update( self, filename, args ):
+		"""Update a rrd with args"""
+
+		return self.perform( 'update', '"' + filename + '"', args )
+
+	def grabLastUpdate( self, filename ):
+		"""Determine the last update time of filename rrd"""
+
+		last_update = 0
+
+		debug_msg( 8, self.binary + ' info "' + filename + '"' )
+
+		my_pipe		= os.popen( self.binary + ' info "' + filename + '"' )
+
+		for line in my_pipe.readlines():
+
+			if line.find( 'last_update') != -1:
+
+				last_update = line.split( ' = ' )[1]
+
+		if my_pipe:
+
+			my_pipe.close()
+
+		if last_update:
+			return last_update
+		else:
+			return 0
+
+	def perform( self, action, filename, args ):
+		"""Perform action on rrd filename with args"""
+
+		arg_string = None
+
+		if type( args ) is not ListType:
+			debug_msg( 8, 'Arguments needs to be of type List' )
+			return 1
+
+		for arg in args:
+
+			if not arg_string:
+
+				arg_string = arg
+			else:
+				arg_string = arg_string + ' ' + arg
+
+		debug_msg( 8, self.binary + ' ' + action + ' ' + filename + ' ' + arg_string  )
+
+		cmd	= os.popen( self.binary + ' ' + action + ' ' + filename + ' ' + arg_string )
+		lines	= cmd.readlines()
+
+		cmd.close()
+
+		for line in lines:
+
+			if line.find( 'ERROR' ) != -1:
+
+				error_msg = string.join( line.split( ' ' )[1:] )
+				debug_msg( 8, error_msg )
+				return 1
+
+		return 0
+
+class XMLProcessor:
+	"""Skeleton class for XML processor's"""
+
+	def run( self ):
+		"""Do main processing of XML here"""
+
+		pass
+
+class TorqueXMLProcessor( XMLProcessor ):
+	"""Main class for processing XML and acting with it"""
+
+	def __init__( self, XMLSource, DataStore ):
+		"""Setup initial XML connection and handlers"""
+
+		#self.myXMLGatherer	= XMLGatherer( ARCHIVE_XMLSOURCE.split( ':' )[0], ARCHIVE_XMLSOURCE.split( ':' )[1] ) 
+		#self.myXMLSource	= self.myXMLGatherer.getFileObject()
+		self.myXMLSource	= XMLSource
+		self.myXMLHandler	= TorqueXMLHandler( DataStore )
+		self.myXMLError		= XMLErrorHandler()
+
+		self.config		= GangliaConfigParser( GMETAD_CONF )
+
+	def run( self ):
+		"""Main XML processing"""
+
+		debug_msg( 1, 'torque_xml_thread(): started.' )
+
+		while( 1 ):
+
+			#self.myXMLSource = self.mXMLGatherer.getFileObject()
+			debug_msg( 1, 'torque_xml_thread(): Parsing..' )
+
+			my_data	= self.myXMLSource.getData()
+
+			try:
+				xml.sax.parseString( my_data, self.myXMLHandler, self.myXMLError )
+			except socket.error, msg:
+				debug_msg( 0, 'ERROR: Socket error in connect to datasource!: %s' %msg )
+				
+			debug_msg( 1, 'torque_xml_thread(): Done parsing.' )
+			debug_msg( 1, 'torque_xml_thread(): Sleeping.. (%ss)' %(str( self.config.getLowestInterval() ) ) )
+			time.sleep( self.config.getLowestInterval() )
+
+class TorqueXMLHandler( xml.sax.handler.ContentHandler ):
+	"""Parse Torque's jobinfo XML from our plugin"""
+
+	jobAttrs = { }
+
+	def __init__( self, datastore ):
+
+		#self.ds			= DataSQLStore( JOB_SQL_DBASE.split( '/' )[0], JOB_SQL_DBASE.split( '/' )[1] )
+		self.ds			= datastore
+		self.jobs_processed	= [ ]
+		self.jobs_to_store	= [ ]
+
+	def startDocument( self ):
+
+		self.heartbeat	= 0
+
+	def startElement( self, name, attrs ):
+		"""
+		This XML will be all gmetric XML
+		so there will be no specific start/end element
+		just one XML statement with all info
+		"""
+		
+		jobinfo = { }
+
+		if name == 'CLUSTER':
+
+			self.clustername = attrs.get( 'NAME', "" )
+
+		elif name == 'METRIC' and self.clustername in ARCHIVE_DATASOURCES:
+
+			metricname = attrs.get( 'NAME', "" )
+
+			if metricname == 'MONARCH-HEARTBEAT':
+				self.heartbeat = attrs.get( 'VAL', "" )
+
+			elif metricname.find( 'MONARCH-JOB' ) != -1:
+
+				job_id	= metricname.split( 'MONARCH-JOB-' )[1].split( '-' )[0]
+				val	= attrs.get( 'VAL', "" )
+
+				if not job_id in self.jobs_processed:
+
+					self.jobs_processed.append( job_id )
+
+				check_change = 0
+
+				if self.jobAttrs.has_key( job_id ):
+
+					check_change = 1
+
+				valinfo = val.split( ' ' )
+
+				for myval in valinfo:
+
+					if len( myval.split( '=' ) ) > 1:
+
+						valname	= myval.split( '=' )[0]
+						value	= myval.split( '=' )[1]
+
+						if valname == 'nodes':
+							value = value.split( ';' )
+
+						jobinfo[ valname ] = value
+
+				if check_change:
+					if self.jobinfoChanged( self.jobAttrs, job_id, jobinfo ) and self.jobAttrs[ job_id ]['status'] in [ 'R', 'Q' ]:
+						self.jobAttrs[ job_id ]['stop_timestamp']	= ''
+						self.jobAttrs[ job_id ]				= self.setJobAttrs( self.jobAttrs[ job_id ], jobinfo )
+						if not job_id in self.jobs_to_store:
+							self.jobs_to_store.append( job_id )
+
+						debug_msg( 6, 'jobinfo for job %s has changed' %job_id )
+				else:
+					self.jobAttrs[ job_id ] = jobinfo
+
+					if not job_id in self.jobs_to_store:
+						self.jobs_to_store.append( job_id )
+
+					debug_msg( 6, 'jobinfo for job %s has changed' %job_id )
+					
+	def endDocument( self ):
+		"""When all metrics have gone, check if any jobs have finished"""
+
+		if self.heartbeat:
+			for jobid, jobinfo in self.jobAttrs.items():
+
+				# This is an old job, not in current jobinfo list anymore
+				# it must have finished, since we _did_ get a new heartbeat
+				#
+				mytime = int( jobinfo['reported'] ) + int( jobinfo['poll_interval'] )
+
+				if (mytime < self.heartbeat) and (jobid not in self.jobs_processed) and (jobinfo['status'] == 'R'):
+
+					if not jobid in self.jobs_processed:
+						self.jobs_processed.append( jobid )
+
+					self.jobAttrs[ jobid ]['status'] = 'F'
+					self.jobAttrs[ jobid ]['stop_timestamp'] = str( mytime )
+
+					if not jobid in self.jobs_to_store:
+						self.jobs_to_store.append( jobid )
+
+			debug_msg( 1, 'torque_xml_thread(): Storing..' )
+
+			for jobid in self.jobs_to_store:
+				if self.jobAttrs[ jobid ]['status'] in [ 'R', 'F' ]:
+
+					self.ds.storeJobInfo( jobid, self.jobAttrs[ jobid ] )
+
+					if self.jobAttrs[ jobid ]['status'] == 'F':
+						del self.jobAttrs[ jobid ]
+
+			debug_msg( 1, 'torque_xml_thread(): Done storing.' )
+
+			self.jobs_processed	= [ ]
+			self.jobs_to_store	= [ ]
+
+	def setJobAttrs( self, old, new ):
+		"""
+		Set new job attributes in old, but not lose existing fields
+		if old attributes doesn't have those
+		"""
+
+		for valname, value in new.items():
+			old[ valname ] = value
+
+		return old
+		
+
+	def jobinfoChanged( self, jobattrs, jobid, jobinfo ):
+		"""
+		Check if jobinfo has changed from jobattrs[jobid]
+		if it's report time is bigger than previous one
+		and it is report time is recent (equal to heartbeat)
+		"""
+
+		ignore_changes = [ 'reported' ]
+
+		if jobattrs.has_key( jobid ):
+
+			for valname, value in jobinfo.items():
+
+				if valname not in ignore_changes:
+
+					if jobattrs[ jobid ].has_key( valname ):
+
+						if value != jobattrs[ jobid ][ valname ]:
+
+							if jobinfo['reported'] > jobattrs[ jobid ][ 'reported' ] and jobinfo['reported'] == self.heartbeat:
+								return 1
+
+					else:
+						return 1
+
+		return 0
+
+class GangliaXMLHandler( xml.sax.handler.ContentHandler ):
+	"""Parse Ganglia's XML"""
+
+	def __init__( self, config, datastore ):
+		"""Setup initial variables and gather info on existing rrd archive"""
+
+		self.config	= config
+		self.clusters	= { }
+		self.ds		= datastore
+
+		debug_msg( 1, 'Checking database..' )
+		self.ds.checkStaleJobs()
+		debug_msg( 1, 'Check done.' )
+		debug_msg( 1, 'Checking rrd archive..' )
+		self.gatherClusters()
+		debug_msg( 1, 'Check done.' )
+
+	def gatherClusters( self ):
+		"""Find all existing clusters in archive dir"""
+
+		archive_dir	= check_dir(ARCHIVE_PATH)
+
+		hosts		= [ ]
+
+		if os.path.exists( archive_dir ):
+
+			dirlist	= os.listdir( archive_dir )
+
+			for item in dirlist:
+
+				clustername = item
+
+				if not self.clusters.has_key( clustername ) and clustername in ARCHIVE_DATASOURCES:
+
+					self.clusters[ clustername ] = RRDHandler( self.config, clustername )
+
+	def startElement( self, name, attrs ):
+		"""Memorize appropriate data from xml start tags"""
+
+		if name == 'GANGLIA_XML':
+
+			self.XMLSource		= attrs.get( 'SOURCE', "" )
+			self.gangliaVersion	= attrs.get( 'VERSION', "" )
+
+			debug_msg( 10, 'Found XML data: source %s version %s' %( self.XMLSource, self.gangliaVersion ) )
+
+		elif name == 'GRID':
+
+			self.gridName	= attrs.get( 'NAME', "" )
+			self.time	= attrs.get( 'LOCALTIME', "" )
+
+			debug_msg( 10, '`-Grid found: %s' %( self.gridName ) )
+
+		elif name == 'CLUSTER':
+
+			self.clusterName	= attrs.get( 'NAME', "" )
+			self.time		= attrs.get( 'LOCALTIME', "" )
+
+			if not self.clusters.has_key( self.clusterName ) and self.clusterName in ARCHIVE_DATASOURCES:
+
+				self.clusters[ self.clusterName ] = RRDHandler( self.config, self.clusterName )
+
+				debug_msg( 10, ' |-Cluster found: %s' %( self.clusterName ) )
+
+		elif name == 'HOST' and self.clusterName in ARCHIVE_DATASOURCES:     
+
+			self.hostName		= attrs.get( 'NAME', "" )
+			self.hostIp		= attrs.get( 'IP', "" )
+			self.hostReported	= attrs.get( 'REPORTED', "" )
+
+			debug_msg( 10, ' | |-Host found: %s - ip %s reported %s' %( self.hostName, self.hostIp, self.hostReported ) )
+
+		elif name == 'METRIC' and self.clusterName in ARCHIVE_DATASOURCES:
+
+			type = attrs.get( 'TYPE', "" )
+			
+			exclude_metric = False
+			
+			for ex_metricstr in ARCHIVE_EXCLUDE_METRICS:
+
+				orig_name = attrs.get( 'NAME', "" )	
+
+				if string.lower( orig_name ) == string.lower( ex_metricstr ):
+				
+					exclude_metric = True
+
+				elif re.match( ex_metricstr, orig_name ):
+
+					exclude_metric = True
+
+			if type not in UNSUPPORTED_ARCHIVE_TYPES and not exclude_metric:
+
+				myMetric		= { }
+				myMetric['name']	= attrs.get( 'NAME', "" )
+				myMetric['val']		= attrs.get( 'VAL', "" )
+				myMetric['time']	= self.hostReported
+
+				self.clusters[ self.clusterName ].memMetric( self.hostName, myMetric )
+
+				debug_msg( 11, ' | | |-metric: %s:%s' %( myMetric['name'], myMetric['val'] ) )
+
+	def storeMetrics( self ):
+		"""Store metrics of each cluster rrd handler"""
+
+		for clustername, rrdh in self.clusters.items():
+
+			ret = rrdh.storeMetrics()
+
+			if ret:
+				debug_msg( 9, 'An error occured while storing metrics for cluster %s' %clustername )
+				return 1
+
+		return 0
+
+class XMLErrorHandler( xml.sax.handler.ErrorHandler ):
+
+	def error( self, exception ):
+		"""Recoverable error"""
+
+		debug_msg( 0, 'Recoverable XML error ' + str( exception ) + ' ignored.' )
+
+	def fatalError( self, exception ):
+		"""Non-recoverable error"""
+
+		exception_str = str( exception )
+
+		# Ignore 'no element found' errors
+		if exception_str.find( 'no element found' ) != -1:
+			debug_msg( 0, 'No XML data found: Socket not (re)connected or datasource not available.' )
+			return 0
+
+		debug_msg( 0, 'FATAL ERROR: Non-recoverable XML error ' + str( exception ) )
+		sys.exit( 1 )
+
+	def warning( self, exception ):
+		"""Warning"""
+
+		debug_msg( 0, 'Warning ' + str( exception ) )
+
+class XMLGatherer:
+	"""Setup a connection and file object to Ganglia's XML"""
+
+	s		= None
+	fd		= None
+	data		= None
+	slot		= None
+
+	# Time since the last update
+	#
+	LAST_UPDATE	= 0
+
+	# Minimum interval between updates
+	#
+	MIN_UPDATE_INT	= 10
+
+	# Is a update occuring now
+	#
+	update_now	= False
+
+	def __init__( self, host, port ):
+		"""Store host and port for connection"""
+
+		self.host	= host
+		self.port	= port
+		self.slot       = threading.Lock()
+
+		self.retrieveData()
+
+	def retrieveData( self ):
+		"""Setup connection to XML source"""
+
+		self.update_now	= True
+
+		self.slot.acquire()
+
+		for res in socket.getaddrinfo( self.host, self.port, socket.AF_UNSPEC, socket.SOCK_STREAM ):
+
+			af, socktype, proto, canonname, sa = res
+
+			try:
+
+				self.s = socket.socket( af, socktype, proto )
+
+			except socket.error, msg:
+
+				self.s = None
+				continue
+
+		    	try:
+
+				self.s.connect( sa )
+
+		    	except socket.error, msg:
+
+				self.disconnect()
+				continue
+
+		    	break
+
+		if self.s is None:
+
+			debug_msg( 0, 'FATAL ERROR: Could not open socket or unable to connect to datasource!' )
+			self.update_now	= False
+			sys.exit( 1 )
+
+		else:
+			#self.s.send( '\n' )
+
+			my_fp			= self.s.makefile( 'r' )
+			my_data			= my_fp.readlines()
+			my_data			= string.join( my_data, '' )
+
+			self.data		= my_data
+
+			self.LAST_UPDATE	= time.time()
+
+		self.slot.release()
+
+		self.update_now	= False
+
+	def disconnect( self ):
+		"""Close socket"""
+
+		if self.s:
+			#self.s.shutdown( 2 )
+			self.s.close()
+			self.s = None
+
+	def __del__( self ):
+		"""Kill the socket before we leave"""
+
+		self.disconnect()
+
+	def reGetData( self ):
+		"""Reconnect"""
+
+		while self.update_now:
+
+			# Must be another update in progress:
+			# Wait until the update is complete
+			#
+			time.sleep( 1 )
+
+		if self.s:
+			self.disconnect()
+
+		self.retrieveData()
+
+	def getData( self ):
+
+		"""Return the XML data"""
+
+		# If more than MIN_UPDATE_INT seconds passed since last data update
+		# update the XML first before returning it
+		#
+
+		cur_time	= time.time()
+
+		if ( cur_time - self.LAST_UPDATE ) > self.MIN_UPDATE_INT:
+
+			self.reGetData()
+
+		while self.update_now:
+
+			# Must be another update in progress:
+			# Wait until the update is complete
+			#
+			time.sleep( 1 )
+			
+		return self.data
+
+	def makeFileDescriptor( self ):
+		"""Make file descriptor that points to our socket connection"""
+
+		self.reconnect()
+
+		if self.s:
+			self.fd = self.s.makefile( 'r' )
+
+	def getFileObject( self ):
+		"""Connect, and return a file object"""
+
+		self.makeFileDescriptor()
+
+		if self.fd:
+			return self.fd
+
+class GangliaXMLProcessor( XMLProcessor ):
+	"""Main class for processing XML and acting with it"""
+
+	def __init__( self, XMLSource, DataStore ):
+		"""Setup initial XML connection and handlers"""
+
+		self.config		= GangliaConfigParser( GMETAD_CONF )
+
+		#self.myXMLGatherer	= XMLGatherer( ARCHIVE_XMLSOURCE.split( ':' )[0], ARCHIVE_XMLSOURCE.split( ':' )[1] ) 
+		#self.myXMLSource	= self.myXMLGatherer.getFileObject()
+		self.myXMLSource	= XMLSource
+		self.ds			= DataStore
+		self.myXMLHandler	= GangliaXMLHandler( self.config, self.ds )
+		self.myXMLError		= XMLErrorHandler()
+
+	def run( self ):
+		"""Main XML processing; start a xml and storethread"""
+
+		xml_thread = threading.Thread( None, self.processXML, 'xmlthread' )
+		store_thread = threading.Thread( None, self.storeMetrics, 'storethread' )
+
+		while( 1 ):
+
+			if not xml_thread.isAlive():
+				# Gather XML at the same interval as gmetad
+
+				# threaded call to: self.processXML()
+				#
+				try:
+					xml_thread = threading.Thread( None, self.processXML, 'xml_thread' )
+					xml_thread.start()
+				except thread.error, msg:
+					debug_msg( 0, 'ERROR: Unable to start xml_thread!: '+str(msg))
+					#return 1
+
+			if not store_thread.isAlive():
+				# Store metrics every .. sec
+
+				# threaded call to: self.storeMetrics()
+				#
+				try:
+					store_thread = threading.Thread( None, self.storeMetrics, 'store_thread' )
+					store_thread.start()
+				except thread.error, msg:
+					debug_msg( 0, 'ERROR: Unable to start store_thread!: '+str(msg))
+					#return 1
+		
+			# Just sleep a sec here, to prevent daemon from going mad. We're all threads here anyway
+			time.sleep( 1 )	
+
+	def storeMetrics( self ):
+		"""Store metrics retained in memory to disk"""
+
+		# Store metrics somewhere between every 360 and 640 seconds
+		#
+		STORE_INTERVAL = random.randint( 360, 640 )
+
+		try:
+			store_metric_thread = threading.Thread( None, self.storeThread, 'store_metric_thread' )
+			store_metric_thread.start()
+		except thread.error, msg:
+			debug_msg( 0, 'ERROR: Unable to start ganglia_store_thread()!: '+str(msg) )
+			return 1
+
+		debug_msg( 1, 'ganglia_store_thread(): started.' )
+
+		debug_msg( 1, 'ganglia_store_thread(): Sleeping.. (%ss)' %STORE_INTERVAL )
+		time.sleep( STORE_INTERVAL )
+		debug_msg( 1, 'ganglia_store_thread(): Done sleeping.' )
+
+		if store_metric_thread.isAlive():
+
+			debug_msg( 1, 'ganglia_store_thread(): storemetricthread() still running, waiting to finish..' )
+			store_metric_thread.join( STORE_TIMEOUT ) # Maximum time is for storing thread to finish
+			debug_msg( 1, 'ganglia_store_thread(): Done waiting.' )
+
+		debug_msg( 1, 'ganglia_store_thread(): finished.' )
+
+		return 0
+
+	def storeThread( self ):
+		"""Actual metric storing thread"""
+
+		debug_msg( 1, 'ganglia_store_metric_thread(): started.' )
+		debug_msg( 1, 'ganglia_store_metric_thread(): Storing data..' )
+		ret = self.myXMLHandler.storeMetrics()
+		if ret > 0:
+			debug_msg( 0, 'ganglia_store_metric_thread(): UNKNOWN ERROR %s while storing Metrics!' %str(ret) )
+		debug_msg( 1, 'ganglia_store_metric_thread(): Done storing.' )
+		debug_msg( 1, 'ganglia_store_metric_thread(): finished.' )
+		
+		return 0
+
+	def processXML( self ):
+		"""Process XML"""
+
+		try:
+			parsethread = threading.Thread( None, self.parseThread, 'parsethread' )
+			parsethread.start()
+		except thread.error, msg:
+			debug_msg( 0, 'ERROR: Unable to start ganglia_xml_thread()!: ' + str(msg) )
+			return 1
+
+		debug_msg( 1, 'ganglia_xml_thread(): started.' )
+
+		debug_msg( 1, 'ganglia_xml_thread(): Sleeping.. (%ss)' %self.config.getLowestInterval() )
+		time.sleep( float( self.config.getLowestInterval() ) )	
+		debug_msg( 1, 'ganglia_xml_thread(): Done sleeping.' )
+
+		if parsethread.isAlive():
+
+			debug_msg( 1, 'ganglia_xml_thread(): parsethread() still running, waiting (%ss) to finish..' %PARSE_TIMEOUT )
+			parsethread.join( PARSE_TIMEOUT ) # Maximum time for XML thread to finish
+			debug_msg( 1, 'ganglia_xml_thread(): Done waiting.' )
+
+		debug_msg( 1, 'ganglia_xml_thread(): finished.' )
+
+		return 0
+
+	def parseThread( self ):
+		"""Actual parsing thread"""
+
+		debug_msg( 1, 'ganglia_parse_thread(): started.' )
+		debug_msg( 1, 'ganglia_parse_thread(): Parsing XML..' )
+		#self.myXMLSource = self.myXMLGatherer.getFileObject()
+		
+		my_data	= self.myXMLSource.getData()
+
+		#print my_data
+
+		try:
+			xml.sax.parseString( my_data, self.myXMLHandler, self.myXMLError )
+		except socket.error, msg:
+			debug_msg( 0, 'ERROR: Socket error in connect to datasource!: %s' %msg )
+
+		debug_msg( 1, 'ganglia_parse_thread(): Done parsing.' )
+		debug_msg( 1, 'ganglia_parse_thread(): finished.' )
+
+		return 0
+
+class GangliaConfigParser:
+
+	sources = [ ]
+
+	def __init__( self, config ):
+		"""Parse some stuff from our gmetad's config, such as polling interval"""
+
+		self.config = config
+		self.parseValues()
+
+	def parseValues( self ):
+		"""Parse certain values from gmetad.conf"""
+
+		readcfg = open( self.config, 'r' )
+
+		for line in readcfg.readlines():
+
+			if line.count( '"' ) > 1:
+
+				if line.find( 'data_source' ) != -1 and line[0] != '#':
+
+					source		= { }
+					source['name']	= line.split( '"' )[1]
+					source_words	= line.split( '"' )[2].split( ' ' )
+
+					for word in source_words:
+
+						valid_interval = 1
+
+						for letter in word:
+
+							if letter not in string.digits:
+
+								valid_interval = 0
+
+						if valid_interval and len(word) > 0:
+
+							source['interval'] = word
+							debug_msg( 9, 'polling interval for %s = %s' %(source['name'], source['interval'] ) )
+	
+					# No interval found, use Ganglia's default	
+					if not source.has_key( 'interval' ):
+						source['interval'] = 15
+						debug_msg( 9, 'polling interval for %s defaulted to 15' %(source['name']) )
+
+					self.sources.append( source )
+
+	def getInterval( self, source_name ):
+		"""Return interval for source_name"""
+
+		for source in self.sources:
+
+			if source['name'] == source_name:
+
+				return source['interval']
+
+		return None
+
+	def getLowestInterval( self ):
+		"""Return the lowest interval of all clusters"""
+
+		lowest_interval = 0
+
+		for source in self.sources:
+
+			if not lowest_interval or source['interval'] <= lowest_interval:
+
+				lowest_interval = source['interval']
+
+		# Return 15 when nothing is found, so that the daemon won't go insane with 0 sec delays
+		if lowest_interval:
+			return lowest_interval
+		else:
+			return 15
+
+class RRDHandler:
+	"""Class for handling RRD activity"""
+
+	myMetrics = { }
+	lastStored = { }
+	timeserials = { }
+	slot = None
+
+	def __init__( self, config, cluster ):
+		"""Setup initial variables"""
+
+		self.block	= 0
+		self.cluster	= cluster
+		self.config	= config
+		self.slot	= threading.Lock()
+		self.rrdm	= RRDMutator( RRDTOOL )
+
+		self.gatherLastUpdates()
+
+	def gatherLastUpdates( self ):
+		"""Populate the lastStored list, containing timestamps of all last updates"""
+
+		cluster_dir = '%s/%s' %( check_dir(ARCHIVE_PATH), self.cluster )
+
+		hosts = [ ]
+
+		if os.path.exists( cluster_dir ):
+
+			dirlist = os.listdir( cluster_dir )
+
+			for dir in dirlist:
+
+				hosts.append( dir )
+
+		for host in hosts:
+
+			host_dir	= cluster_dir + '/' + host
+			dirlist		= os.listdir( host_dir )
+
+			for dir in dirlist:
+
+				if not self.timeserials.has_key( host ):
+
+					self.timeserials[ host ] = [ ]
+
+				self.timeserials[ host ].append( dir )
+
+			last_serial = self.getLastRrdTimeSerial( host )
+
+			if last_serial:
+
+				metric_dir = cluster_dir + '/' + host + '/' + last_serial
+
+				if os.path.exists( metric_dir ):
+
+					dirlist = os.listdir( metric_dir )
+
+					for file in dirlist:
+
+						metricname = file.split( '.rrd' )[0]
+
+						if not self.lastStored.has_key( host ):
+
+							self.lastStored[ host ] = { }
+
+						self.lastStored[ host ][ metricname ] = self.rrdm.grabLastUpdate( metric_dir + '/' + file )
+
+	def getClusterName( self ):
+		"""Return clustername"""
+
+		return self.cluster
+
+	def memMetric( self, host, metric ):
+		"""Store metric from host in memory"""
+
+		# <ATOMIC>
+		#
+		self.slot.acquire()
+		
+		if self.myMetrics.has_key( host ):
+
+			if self.myMetrics[ host ].has_key( metric['name'] ):
+
+				for mymetric in self.myMetrics[ host ][ metric['name'] ]:
+
+					if mymetric['time'] == metric['time']:
+
+						# Allready have this metric, abort
+						self.slot.release()
+						return 1
+			else:
+				self.myMetrics[ host ][ metric['name'] ] = [ ]
+		else:
+			self.myMetrics[ host ]				= { }
+			self.myMetrics[ host ][ metric['name'] ]	= [ ]
+
+		# Push new metric onto stack
+		# atomic code; only 1 thread at a time may access the stack
+
+		self.myMetrics[ host ][ metric['name'] ].append( metric )
+
+		self.slot.release()
+		#
+		# </ATOMIC>
+
+	def makeUpdateList( self, host, metriclist ):
+		"""
+		Make a list of update values for rrdupdate
+		but only those that we didn't store before
+		"""
+
+		update_list	= [ ]
+		metric		= None
+
+		while len( metriclist ) > 0:
+
+			metric = metriclist.pop( 0 )
+
+			if self.checkStoreMetric( host, metric ):
+
+				update_list.append( '%s:%s' %( metric['time'], metric['val'] ) )
+
+		return update_list
+
+	def checkStoreMetric( self, host, metric ):
+		"""Check if supplied metric if newer than last one stored"""
+
+		if self.lastStored.has_key( host ):
+
+			if self.lastStored[ host ].has_key( metric['name'] ):
+
+				if metric['time'] <= self.lastStored[ host ][ metric['name'] ]:
+
+					# This is old
+					return 0
+
+		return 1
+
+	def memLastUpdate( self, host, metricname, metriclist ):
+		"""
+		Memorize the time of the latest metric from metriclist
+		but only if it wasn't allready memorized
+		"""
+
+		if not self.lastStored.has_key( host ):
+			self.lastStored[ host ] = { }
+
+		last_update_time = 0
+
+		for metric in metriclist:
+
+			if metric['name'] == metricname:
+
+				if metric['time'] > last_update_time:
+
+					last_update_time = metric['time']
+
+		if self.lastStored[ host ].has_key( metricname ):
+			
+			if last_update_time <= self.lastStored[ host ][ metricname ]:
+				return 1
+
+		self.lastStored[ host ][ metricname ] = last_update_time
+
+	def storeMetrics( self ):
+		"""
+		Store all metrics from memory to disk
+		and do it to the RRD's in appropriate timeperiod directory
+		"""
+
+		for hostname, mymetrics in self.myMetrics.items():	
+
+			for metricname, mymetric in mymetrics.items():
+
+				metrics_to_store = [ ]
+
+				# Pop metrics from stack for storing until none is left
+				# atomic code: only 1 thread at a time may access myMetrics
+
+				# <ATOMIC>
+				#
+				self.slot.acquire() 
+
+				while len( self.myMetrics[ hostname ][ metricname ] ) > 0:
+
+					if len( self.myMetrics[ hostname ][ metricname ] ) > 0:
+
+						try:
+							metrics_to_store.append( self.myMetrics[ hostname ][ metricname ].pop( 0 ) )
+						except IndexError, msg:
+
+							# Somehow sometimes myMetrics[ hostname ][ metricname ]
+							# is still len 0 when the statement is executed.
+							# Just ignore indexerror's..
+							pass
+
+				self.slot.release()
+				#
+				# </ATOMIC>
+
+				# Create a mapping table, each metric to the period where it should be stored
+				#
+				metric_serial_table = self.determineSerials( hostname, metricname, metrics_to_store )
+
+				update_rets = [ ]
+
+				for period, pmetric in metric_serial_table.items():
+
+					create_ret = self.createCheck( hostname, metricname, period )	
+
+					update_ret = self.update( hostname, metricname, period, pmetric )
+
+					if update_ret == 0:
+
+						debug_msg( 9, 'stored metric %s for %s' %( hostname, metricname ) )
+					else:
+						debug_msg( 9, 'metric update failed' )
+
+					update_rets.append( create_ret )
+					update_rets.append( update_ret )
+
+				# Lets ignore errors here for now, we need to make sure last update time
+				# is correct!
+				#
+				#if not (1) in update_rets:
+
+				self.memLastUpdate( hostname, metricname, metrics_to_store )
+
+	def makeTimeSerial( self ):
+		"""Generate a time serial. Seconds since epoch"""
+
+		# Seconds since epoch
+		mytime = int( time.time() )
+
+		return mytime
+
+	def makeRrdPath( self, host, metricname, timeserial ):
+		"""Make a RRD location/path and filename"""
+
+		rrd_dir		= '%s/%s/%s/%s'	%( check_dir(ARCHIVE_PATH), self.cluster, host, timeserial )
+		rrd_file	= '%s/%s.rrd'	%( rrd_dir, metricname )
+
+		return rrd_dir, rrd_file
+
+	def getLastRrdTimeSerial( self, host ):
+		"""Find the last timeserial (directory) for this host"""
+
+		newest_timeserial = 0
+
+		for dir in self.timeserials[ host ]:
+
+			valid_dir = 1
+
+			for letter in dir:
+				if letter not in string.digits:
+					valid_dir = 0
+
+			if valid_dir:
+				timeserial = dir
+				if timeserial > newest_timeserial:
+					newest_timeserial = timeserial
+
+		if newest_timeserial:
+			return newest_timeserial
+		else:
+			return 0
+
+	def determinePeriod( self, host, check_serial ):
+		"""Determine to which period (directory) this time(serial) belongs"""
+
+		period_serial = 0
+
+		if self.timeserials.has_key( host ):
+
+			for serial in self.timeserials[ host ]:
+
+				if check_serial >= serial and period_serial < serial:
+
+					period_serial = serial
+
+		return period_serial
+
+	def determineSerials( self, host, metricname, metriclist ):
+		"""
+		Determine the correct serial and corresponding rrd to store
+		for a list of metrics
+		"""
+
+		metric_serial_table = { }
+
+		for metric in metriclist:
+
+			if metric['name'] == metricname:
+
+				period		= self.determinePeriod( host, metric['time'] )	
+
+				archive_secs	= ARCHIVE_HOURS_PER_RRD * (60 * 60)
+
+				if (int( metric['time'] ) - int( period ) ) > archive_secs:
+
+					# This one should get it's own new period
+					period = metric['time']
+
+					if not self.timeserials.has_key( host ):
+						self.timeserials[ host ] = [ ]
+
+					self.timeserials[ host ].append( period )
+
+				if not metric_serial_table.has_key( period ):
+
+					metric_serial_table[ period ] = [ ]
+
+				metric_serial_table[ period ].append( metric )
+
+		return metric_serial_table
+
+	def createCheck( self, host, metricname, timeserial ):
+		"""Check if an rrd allready exists for this metric, create if not"""
+
+		debug_msg( 9, 'rrdcreate: using timeserial %s for %s/%s' %( timeserial, host, metricname ) )
+		
+		rrd_dir, rrd_file = self.makeRrdPath( host, metricname, timeserial )
+
+		if not os.path.exists( rrd_dir ):
+
+			try:
+				os.makedirs( rrd_dir )
+
+			except os.OSError, msg:
+
+				if msg.find( 'File exists' ) != -1:
+
+					# Ignore exists errors
+					pass
+
+				else:
+
+					print msg
+					return
+
+			debug_msg( 9, 'created dir %s' %( str(rrd_dir) ) )
+
+		if not os.path.exists( rrd_file ):
+
+			interval	= self.config.getInterval( self.cluster )
+			heartbeat	= 8 * int( interval )
+
+			params		= [ ]
+
+			params.append( '--step' )
+			params.append( str( interval ) )
+
+			params.append( '--start' )
+			params.append( str( int( timeserial ) - 1 ) )
+
+			params.append( 'DS:sum:GAUGE:%d:U:U' %heartbeat )
+			params.append( 'RRA:AVERAGE:0.5:1:%s' %(ARCHIVE_HOURS_PER_RRD * 240) )
+
+			self.rrdm.create( str(rrd_file), params )
+
+			debug_msg( 9, 'created rrd %s' %( str(rrd_file) ) )
+
+	def update( self, host, metricname, timeserial, metriclist ):
+		"""
+		Update rrd file for host with metricname
+		in directory timeserial with metriclist
+		"""
+
+		debug_msg( 9, 'rrdupdate: using timeserial %s for %s/%s' %( timeserial, host, metricname ) )
+
+		rrd_dir, rrd_file	= self.makeRrdPath( host, metricname, timeserial )
+
+		update_list		= self.makeUpdateList( host, metriclist )
+
+		if len( update_list ) > 0:
+			ret = self.rrdm.update( str(rrd_file), update_list )
+
+			if ret:
+				return 1
+		
+			debug_msg( 9, 'updated rrd %s with %s' %( str(rrd_file), string.join( update_list ) ) )
+
+		return 0
+
+def daemon():
+	"""daemonized threading"""
+
+	# Fork the first child
+	#
+	pid = os.fork()
+
+	if pid > 0:
+
+		sys.exit(0)  # end parent
+
+	# creates a session and sets the process group ID 
+	#
+	os.setsid()
+
+	# Fork the second child
+	#
+	pid = os.fork()
+
+	if pid > 0:
+
+		sys.exit(0)  # end parent
+
+	# Go to the root directory and set the umask
+	#
+	os.chdir('/')
+	os.umask(0)
+
+	sys.stdin.close()
+	sys.stdout.close()
+	sys.stderr.close()
+
+	os.open('/dev/null', os.O_RDWR)
+	os.dup2(0, 1)
+	os.dup2(0, 2)
+
+	run()
+
+def run():
+	"""Threading start"""
+
+	myXMLSource		= XMLGatherer( ARCHIVE_XMLSOURCE.split( ':' )[0], ARCHIVE_XMLSOURCE.split( ':' )[1] )
+	myDataStore		= DataSQLStore( JOB_SQL_DBASE.split( '/' )[0], JOB_SQL_DBASE.split( '/' )[1] )
+
+	myTorqueProcessor	= TorqueXMLProcessor( myXMLSource, myDataStore )
+	myGangliaProcessor	= GangliaXMLProcessor( myXMLSource, myDataStore )
+
+	try:
+		torque_xml_thread	= threading.Thread( None, myTorqueProcessor.run, 'torque_proc_thread' )
+		ganglia_xml_thread	= threading.Thread( None, myGangliaProcessor.run, 'ganglia_proc_thread' )
+
+		torque_xml_thread.start()
+		ganglia_xml_thread.start()
+		
+	except thread.error, msg:
+		debug_msg( 0, 'FATAL ERROR: Unable to start main threads!: '+ str(msg) )
+		syslog.closelog()
+		sys.exit(1)
+		
+	debug_msg( 0, 'main threading started.' )
+
+def main():
+	"""Program startup"""
+
+	if not processArgs( sys.argv[1:] ):
+		sys.exit( 1 )
+
+	if( DAEMONIZE and USE_SYSLOG ):
+		syslog.openlog( 'jobarchived', syslog.LOG_NOWAIT, SYSLOG_FACILITY )
+
+	if DAEMONIZE:
+		daemon()
+	else:
+		run()
+
+#
+# Global functions
+#
+
+def check_dir( directory ):
+	"""Check if directory is a proper directory. I.e.: Does _not_ end with a '/'"""
+
+	if directory[-1] == '/':
+		directory = directory[:-1]
+
+	return directory
+
+def reqtime2epoch( rtime ):
+
+	(hours, minutes, seconds )	= rtime.split( ':' )
+
+	etime	= int(seconds)
+	etime	= etime + ( int(minutes) * 60 )
+	etime	= etime + ( int(hours) * 60 * 60 )
+
+	return etime
+
+def debug_msg( level, msg ):
+	"""Only print msg if correct levels"""
+
+	if (not DAEMONIZE and DEBUG_LEVEL >= level):
+		sys.stderr.write( printTime() + ' - ' + msg + '\n' )
+	
+	if (DAEMONIZE and USE_SYSLOG and SYSLOG_LEVEL >= level):
+		syslog.syslog( msg )
+
+def printTime( ):
+	"""Print current time in human readable format"""
+
+	return time.strftime("%a %d %b %Y %H:%M:%S")
+
+# Ooohh, someone started me! Let's go..
+if __name__ == '__main__':
+	main()
Index: /tags/0.2/jobmond/examples/lisa_production.conf
===================================================================
--- /tags/0.2/jobmond/examples/lisa_production.conf	(revision 325)
+++ /tags/0.2/jobmond/examples/lisa_production.conf	(revision 325)
@@ -0,0 +1,9 @@
+[DEFAULT]
+DEBUG_LEVEL		: 0
+DAEMONIZE		: 1
+BATCH_API		: pbs
+BATCH_SERVER		: batch-ng.irc.sara.nl
+BATCH_POLL_INTERVAL	: 30
+GMOND_CONF		: /etc/gmond/lisa_production.conf
+DETECT_TIME_DIFFS	: 1
+BATCH_HOST_TRANSLATE	: /gb2/gb/, /ib/gb/
Index: /tags/0.2/jobmond/examples/lisa_test.conf
===================================================================
--- /tags/0.2/jobmond/examples/lisa_test.conf	(revision 325)
+++ /tags/0.2/jobmond/examples/lisa_test.conf	(revision 325)
@@ -0,0 +1,9 @@
+[DEFAULT]
+DEBUG_LEVEL		: 0
+DAEMONIZE		: 1
+BATCH_API		: pbs
+BATCH_SERVER		: gb-r7n15.irc.sara.nl
+BATCH_POLL_INTERVAL	: 10
+GMOND_CONF		: /etc/gmond/lisa_test.conf
+DETECT_TIME_DIFFS	: 1
+BATCH_HOST_TRANSLATE    :
Index: /tags/0.2/jobmond/jobmond.conf
===================================================================
--- /tags/0.2/jobmond/jobmond.conf	(revision 325)
+++ /tags/0.2/jobmond/jobmond.conf	(revision 325)
@@ -0,0 +1,55 @@
+[DEFAULT]
+# Specify debugging level here;
+#
+# 10 = gemtric cmd's
+#
+DEBUG_LEVEL		: 0
+
+# Wether or not to run as a daemon in background
+#
+DAEMONIZE		: 1
+
+# What Batch type is the system
+# 
+# Currently supported: pbs, sge (experimental)
+#
+BATCH_API		: pbs
+
+# Which Batch server to monitor
+#
+BATCH_SERVER		: localhost
+
+# Which queue(s) to report jobs of
+# (optional and only supported for pbs)
+#
+#QUEUE			: long, short
+
+# How many seconds interval for polling of jobs
+#
+# this will effect directly how accurate the
+# end time of a job can be determined
+#
+BATCH_POLL_INTERVAL	: 30
+
+# Location of gmond.conf
+#
+# Default: /etc/gmond.conf
+#
+GMOND_CONF		: /etc/gmond.conf
+
+# Wether or not to detect differences in
+# time from Torque server and local time.
+#
+# Ideally both machines (if not the same)
+# should have the same time (via ntp or whatever)
+#
+DETECT_TIME_DIFFS	: 1
+
+# Regexp style hostname translation
+#
+# Usefull if your Batch hostnames are not the same as your
+# Ganglia hostnames (different network interfaces)
+#
+# Syntax: /orig/new/, /orig/new/
+#
+BATCH_HOST_TRANSLATE	: 
Index: /tags/0.2/jobmond/jobmond.py
===================================================================
--- /tags/0.2/jobmond/jobmond.py	(revision 325)
+++ /tags/0.2/jobmond/jobmond.py	(revision 325)
@@ -0,0 +1,814 @@
+#!/usr/bin/env python
+#
+# This file is part of Jobmonarch
+#
+# Copyright (C) 2006  Ramon Bastiaans
+# 
+# Jobmonarch 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 2 of the License, or
+# (at your option) any later version.
+# 
+# Jobmonarch 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, write to the Free Software
+# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
+# 
+# SVN $Id$
+#
+
+import sys, getopt, ConfigParser
+
+import xml, xml.sax
+from xml.sax import saxutils, make_parser
+from xml.sax import make_parser
+from xml.sax.handler import feature_namespaces
+
+def usage():
+
+	print
+	print 'usage: jobmond [options]'
+	print 'options:'
+	print '      --config, -c      configuration file'
+	print '      --pidfile, -p     pid file'
+	print '      --help, -h        help'
+	print
+
+
+def processArgs( args ):
+
+	SHORT_L = 'c:'
+	LONG_L = 'config='
+
+	global PIDFILE
+	PIDFILE = None
+	config_filename = '/etc/jobmond.conf'
+
+	try:
+
+		opts, args = getopt.getopt( args, SHORT_L, LONG_L )
+
+	except getopt.GetoptError, detail:
+
+		print detail
+		usage()
+		sys.exit(1)
+
+	for opt, value in opts:
+
+		if opt in [ '--config', '-c' ]:
+		
+			config_filename = value
+
+		if opt in [ '--pidfile', '-p' ]:
+
+			PIDFILE = value
+		
+		if opt in [ '--help', '-h' ]:
+ 
+			usage()
+			sys.exit(1)
+
+	return loadConfig( config_filename )
+
+def loadConfig( filename ):
+
+        def getlist( cfg_string ):
+
+                my_list = [ ]
+
+                for item_txt in cfg_string.split( ',' ):
+
+                        sep_char = None
+
+                        item_txt = item_txt.strip()
+
+                        for s_char in [ "'", '"' ]:
+
+                                if item_txt.find( s_char ) != -1:
+
+                                        if item_txt.count( s_char ) != 2:
+
+                                                print 'Missing quote: %s' %item_txt
+                                                sys.exit( 1 )
+
+                                        else:
+
+                                                sep_char = s_char
+                                                break
+
+                        if sep_char:
+
+                                item_txt = item_txt.split( sep_char )[1]
+
+                        my_list.append( item_txt )
+
+                return my_list
+
+	cfg = ConfigParser.ConfigParser()
+
+	cfg.read( filename )
+
+	global DEBUG_LEVEL, DAEMONIZE, BATCH_SERVER, BATCH_POLL_INTERVAL, GMOND_CONF, DETECT_TIME_DIFFS, BATCH_HOST_TRANSLATE, BATCH_API, QUEUE
+
+	DEBUG_LEVEL = cfg.getint( 'DEFAULT', 'DEBUG_LEVEL' )
+
+	DAEMONIZE = cfg.getboolean( 'DEFAULT', 'DAEMONIZE' )
+
+	try:
+
+		BATCH_SERVER = cfg.get( 'DEFAULT', 'BATCH_SERVER' )
+
+	except ConfigParser.NoOptionError:
+
+		# Backwards compatibility for old configs
+		#
+
+		BATCH_SERVER = cfg.get( 'DEFAULT', 'TORQUE_SERVER' )
+		api_guess = 'pbs'
+	
+	try:
+	
+		BATCH_POLL_INTERVAL = cfg.getint( 'DEFAULT', 'BATCH_POLL_INTERVAL' )
+
+	except ConfigParser.NoOptionError:
+
+		# Backwards compatibility for old configs
+		#
+
+		BATCH_POLL_INTERVAL = cfg.getint( 'DEFAULT', 'TORQUE_POLL_INTERVAL' )
+		api_guess = 'pbs'
+		
+	GMOND_CONF = cfg.get( 'DEFAULT', 'GMOND_CONF' )
+
+	DETECT_TIME_DIFFS = cfg.getboolean( 'DEFAULT', 'DETECT_TIME_DIFFS' )
+
+	BATCH_HOST_TRANSLATE = getlist( cfg.get( 'DEFAULT', 'BATCH_HOST_TRANSLATE' ) )
+
+	try:
+
+		BATCH_API = cfg.get( 'DEFAULT', 'BATCH_API' )
+
+	except ConfigParser.NoOptionError, detail:
+
+		if BATCH_SERVER and api_guess:
+			BATCH_API = api_guess
+		else:
+			debug_msg( 0, "fatal error: BATCH_API not set and can't make guess" )
+			sys.exit( 1 )
+
+	try:
+
+		QUEUE = getlist( cfg.get( 'DEFAULT', 'QUEUE' ) )
+
+	except ConfigParser.NoOptionError, detail:
+
+		QUEUE = None
+	
+	return True
+
+
+import time, os, socket, string, re
+
+METRIC_MAX_VAL_LEN = 900
+
+class DataProcessor:
+	"""Class for processing of data"""
+
+	binary = '/usr/bin/gmetric'
+
+	def __init__( self, binary=None ):
+		"""Remember alternate binary location if supplied"""
+
+		if binary:
+			self.binary = binary
+
+		# Timeout for XML
+		#
+		# From ganglia's documentation:
+		#
+		# 'A metric will be deleted DMAX seconds after it is received, and
+	        # DMAX=0 means eternal life.'
+
+		self.dmax = str( int( int( BATCH_POLL_INTERVAL ) * 2 ) )
+
+		try:
+			gmond_file = GMOND_CONF
+
+		except NameError:
+			gmond_file = '/etc/gmond.conf'
+
+		if not os.path.exists( gmond_file ):
+			debug_msg( 0, gmond_file + ' does not exist' )
+			sys.exit( 1 )
+
+		incompatible = self.checkGmetricVersion()
+
+		if incompatible:
+			debug_msg( 0, 'Gmetric version not compatible, pls upgrade to at least 3.0.1' )
+			sys.exit( 1 )
+
+	def checkGmetricVersion( self ):
+		"""
+		Check version of gmetric is at least 3.0.1
+		for the syntax we use
+		"""
+
+		global METRIC_MAX_VAL_LEN
+
+		for line in os.popen( self.binary + ' --version' ).readlines():
+
+			line = line.split( ' ' )
+
+			if len( line ) == 2 and str(line).find( 'gmetric' ) != -1:
+			
+				gmetric_version = line[1].split( '\n' )[0]
+
+				version_major = int( gmetric_version.split( '.' )[0] )
+				version_minor = int( gmetric_version.split( '.' )[1] )
+				version_patch = int( gmetric_version.split( '.' )[2] )
+
+				incompatible = 0
+
+				if version_major < 3:
+
+					incompatible = 1
+				
+				elif version_major == 3:
+
+					if version_minor == 0:
+
+						if version_patch < 1:
+						
+							incompatible = 1
+
+						if version_patch < 3:
+
+							METRIC_MAX_VAL_LEN = 900
+
+						elif version_patch >= 3:
+
+							METRIC_MAX_VAL_LEN = 1400
+
+		return incompatible
+
+	def multicastGmetric( self, metricname, metricval, valtype='string' ):
+		"""Call gmetric binary and multicast"""
+
+		cmd = self.binary
+
+		try:
+			cmd = cmd + ' -c' + GMOND_CONF
+		except NameError:
+			debug_msg( 10, 'Assuming /etc/gmond.conf for gmetric cmd (ommitting)' )
+
+		cmd = cmd + ' -n' + str( metricname )+ ' -v"' + str( metricval )+ '" -t' + str( valtype ) + ' -d' + str( self.dmax )
+
+		debug_msg( 10, printTime() + ' ' + cmd )
+		os.system( cmd )
+
+class DataGatherer:
+
+	"""Skeleton class for batch system DataGatherer"""
+
+	def printJobs( self, jobs ):
+		"""Print a jobinfo overview"""
+
+		for name, attrs in self.jobs.items():
+
+			print 'job %s' %(name)
+
+			for name, val in attrs.items():
+
+				print '\t%s = %s' %( name, val )
+
+	def printJob( self, jobs, job_id ):
+		"""Print job with job_id from jobs"""
+
+		print 'job %s' %(job_id)
+
+		for name, val in jobs[ job_id ].items():
+
+			print '\t%s = %s' %( name, val )
+
+        def daemon( self ):
+                """Run as daemon forever"""
+
+                # Fork the first child
+                #
+                pid = os.fork()
+                if pid > 0:
+                        sys.exit(0)  # end parent
+
+                # creates a session and sets the process group ID
+                #
+                os.setsid()
+
+                # Fork the second child
+                #
+                pid = os.fork()
+                if pid > 0:
+                        sys.exit(0)  # end parent
+
+		write_pidfile()
+
+                # Go to the root directory and set the umask
+                #
+                os.chdir('/')
+                os.umask(0)
+
+                sys.stdin.close()
+                sys.stdout.close()
+                sys.stderr.close()
+
+                os.open('/dev/null', os.O_RDWR)
+                os.dup2(0, 1)
+                os.dup2(0, 2)
+
+                self.run()
+
+        def run( self ):
+                """Main thread"""
+
+                while ( 1 ):
+		
+			self.jobs = self.getJobData( self.jobs )
+			self.submitJobData( self.jobs )
+			time.sleep( BATCH_POLL_INTERVAL )	
+
+class SgeQstatXMLParser(xml.sax.handler.ContentHandler):
+
+	"""Babu Sundaram's experimental SGE qstat XML parser"""
+
+	def __init__(self, qstatinxml):
+
+		self.qstatfile = qstatinxml
+		self.attribs = {}
+		self.value = ''
+		self.jobID = ''
+		self.currentJobInfo = ''
+		self.job_list = []
+		self.EOFFlag = 0
+		self.jobinfoCount = 0
+
+
+	def startElement(self, name, attrs):
+
+		if name == 'job_list':
+			self.currentJobInfo = 'Status=' + attrs.get('state', None) + ' '
+		elif name == 'job_info':
+			self.job_list = []
+			self.jobinfoCount += 1
+
+	def characters(self, ch):
+
+		self.value = self.value + ch
+
+	def endElement(self, name):
+
+		if len(self.value.strip()) > 0 :
+
+			self.currentJobInfo += name + '=' + self.value.strip() + ' '         
+		elif name != 'job_list':
+
+			self.currentJobInfo += name + '=Unknown '
+
+		if name == 'JB_job_number':
+
+			self.jobID = self.value.strip()
+			self.job_list.append(self.jobID)          
+
+		if name == 'job_list':
+
+			if self.attribs.has_key(self.jobID) == False:
+				self.attribs[self.jobID] = self.currentJobInfo
+			elif self.attribs.has_key(self.jobID) and self.attribs[self.jobID] != self.currentJobInfo:
+				self.attribs[self.jobID] = self.currentJobInfo
+			self.currentJobInfo = ''
+			self.jobID = ''
+
+		elif name == 'job_info' and self.jobinfoCount == 2:
+
+			deljobs = []
+			for id in self.attribs:
+				try:
+					self.job_list.index(str(id))
+				except ValueError:
+					deljobs.append(id)
+			for i in deljobs:
+				del self.attribs[i]
+			deljobs = []
+			self.jobinfoCount = 0
+
+		self.value = ''
+
+class SgeDataGatherer(DataGatherer):
+
+	jobs = { }
+
+	def __init__( self ):
+		"""Setup appropriate variables"""
+
+		self.jobs = { }
+		self.timeoffset = 0
+		self.dp = DataProcessor()
+		self.initSgeJobInfo()
+
+	def initSgeJobInfo( self ):
+		"""This is outside the scope of DRMAA; Get the current jobs in SGE"""
+		"""This is a hack because we cant get info about jobs beyond"""
+		"""those in the current DRMAA session"""
+
+		self.qstatparser = SgeQstatXMLParser( SGE_QSTAT_XML_FILE )
+
+		# Obtain the qstat information from SGE in XML format
+		# This would change to DRMAA-specific calls from 6.0u9
+
+	def getJobData(self):
+		"""Gather all data on current jobs in SGE"""
+
+		# Get the information about the current jobs in the SGE queue
+		info = os.popen("qstat -ext -xml").readlines()
+		f = open(SGE_QSTAT_XML_FILE,'w')
+		for lines in info:
+			f.write(lines)
+		f.close()
+
+		# Parse the input
+		f = open(self.qstatparser.qstatfile, 'r')
+		xml.sax.parse(f, self.qstatparser)
+		f.close()
+
+		self.cur_time = time.time()
+
+		return self.qstatparser.attribs
+
+	def submitJobData(self):
+		"""Submit job info list"""
+
+		self.dp.multicastGmetric( 'MONARCH-HEARTBEAT', str( int( int( self.cur_time ) + int( self.timeoffset ) ) ) )
+		# Now let's spread the knowledge
+		#
+		metric_increment = 0
+		for jobid, jobattrs in self.qstatparser.attribs.items():
+
+			self.dp.multicastGmetric( 'MONARCH-JOB-' + jobid + '-' + str(metric_increment), jobattrs)
+
+class PbsDataGatherer(DataGatherer):
+
+	"""This is the DataGatherer for PBS and Torque"""
+
+	global PBSQuery
+
+	def __init__( self ):
+		"""Setup appropriate variables"""
+
+		self.jobs = { }
+		self.timeoffset = 0
+		self.dp = DataProcessor()
+		self.initPbsQuery()
+
+	def initPbsQuery( self ):
+
+		self.pq = None
+		if( BATCH_SERVER ):
+			self.pq = PBSQuery( BATCH_SERVER )
+		else:
+			self.pq = PBSQuery()
+
+	def getAttr( self, attrs, name ):
+		"""Return certain attribute from dictionary, if exists"""
+
+		if attrs.has_key( name ):
+			return attrs[name]
+		else:
+			return ''
+
+	def jobDataChanged( self, jobs, job_id, attrs ):
+		"""Check if job with attrs and job_id in jobs has changed"""
+
+		if jobs.has_key( job_id ):
+			oldData = jobs[ job_id ]	
+		else:
+			return 1
+
+		for name, val in attrs.items():
+
+			if oldData.has_key( name ):
+
+				if oldData[ name ] != attrs[ name ]:
+
+					return 1
+
+			else:
+				return 1
+
+		return 0
+
+	def getJobData( self, known_jobs ):
+		"""Gather all data on current jobs in Torque"""
+
+		if len( known_jobs ) > 0:
+			jobs = known_jobs
+		else:
+			jobs = { }
+
+		#self.initPbsQuery()
+	
+		#print self.pq.getnodes()
+	
+		joblist = {}
+		while len(joblist) == 0:
+			try:
+				joblist = self.pq.getjobs()
+			except PBSError:
+				time.sleep( TORQUE_POLL_INTERVAL )
+
+		self.cur_time = time.time()
+
+		jobs_processed = [ ]
+
+		#self.printJobs( joblist )
+
+		for name, attrs in joblist.items():
+
+			job_id = name.split( '.' )[0]
+
+			jobs_processed.append( job_id )
+
+			name = self.getAttr( attrs, 'Job_Name' )
+			queue = self.getAttr( attrs, 'queue' )
+
+			if QUEUE:
+
+				if QUEUE != queue:
+
+					continue
+
+			owner = self.getAttr( attrs, 'Job_Owner' ).split( '@' )[0]
+			requested_time = self.getAttr( attrs, 'Resource_List.walltime' )
+			requested_memory = self.getAttr( attrs, 'Resource_List.mem' )
+
+			mynoderequest = self.getAttr( attrs, 'Resource_List.nodes' )
+
+			ppn = ''
+
+			if mynoderequest.find( ':' ) != -1 and mynoderequest.find( 'ppn' ) != -1:
+
+				mynoderequest_fields = mynoderequest.split( ':' )
+
+				for mynoderequest_field in mynoderequest_fields:
+
+					if mynoderequest_field.find( 'ppn' ) != -1:
+
+						ppn = mynoderequest_field.split( 'ppn=' )[1]
+
+			status = self.getAttr( attrs, 'job_state' )
+
+			queued_timestamp = self.getAttr( attrs, 'ctime' )
+
+			if status == 'R':
+				start_timestamp = self.getAttr( attrs, 'mtime' )
+				nodes = self.getAttr( attrs, 'exec_host' ).split( '+' )
+
+				nodeslist = [ ]
+
+				for node in nodes:
+					host = node.split( '/' )[0]
+
+					if nodeslist.count( host ) == 0:
+
+						for translate_pattern in BATCH_HOST_TRANSLATE:
+
+							if translate_pattern.find( '/' ) != -1:
+
+								translate_orig = translate_pattern.split( '/' )[1]
+								translate_new = translate_pattern.split( '/' )[2]
+
+								host = re.sub( translate_orig, translate_new, host )
+				
+						if not host in nodeslist:
+				
+							nodeslist.append( host )
+
+				if DETECT_TIME_DIFFS:
+
+					# If a job start if later than our current date,
+					# that must mean the Torque server's time is later
+					# than our local time.
+				
+					if int(start_timestamp) > int( int(self.cur_time) + int(self.timeoffset) ):
+
+						self.timeoffset = int( int(start_timestamp) - int(self.cur_time) )
+
+			elif status == 'Q':
+				start_timestamp = ''
+				count_mynodes = 0
+				numeric_node = 1
+
+				for node in mynoderequest.split( '+' ):
+
+					nodepart = node.split( ':' )[0]
+
+					for letter in nodepart:
+
+						if letter not in string.digits:
+
+							numeric_node = 0
+
+					if not numeric_node:
+						count_mynodes = count_mynodes + 1
+					else:
+						count_mynodes = count_mynodes + int( nodepart )
+						
+				nodeslist = str( count_mynodes )
+			else:
+				start_timestamp = ''
+				nodeslist = ''
+
+			myAttrs = { }
+			myAttrs['name'] = str( name )
+			myAttrs['queue'] = str( queue )
+			myAttrs['owner'] = str( owner )
+			myAttrs['requested_time'] = str( requested_time )
+			myAttrs['requested_memory'] = str( requested_memory )
+			myAttrs['ppn'] = str( ppn )
+			myAttrs['status'] = str( status )
+			myAttrs['start_timestamp'] = str( start_timestamp )
+			myAttrs['queued_timestamp'] = str( queued_timestamp )
+			myAttrs['reported'] = str( int( int( self.cur_time ) + int( self.timeoffset ) ) )
+			myAttrs['nodes'] = nodeslist
+			myAttrs['domain'] = string.join( socket.getfqdn().split( '.' )[1:], '.' )
+			myAttrs['poll_interval'] = str( BATCH_POLL_INTERVAL )
+
+			if self.jobDataChanged( jobs, job_id, myAttrs ) and myAttrs['status'] in [ 'R', 'Q' ]:
+				jobs[ job_id ] = myAttrs
+
+				#debug_msg( 10, printTime() + ' job %s state changed' %(job_id) )
+
+		for id, attrs in jobs.items():
+
+			if id not in jobs_processed:
+
+				# This one isn't there anymore; toedeledoki!
+				#
+				del jobs[ id ]
+
+		return jobs
+
+	def submitJobData( self, jobs ):
+		"""Submit job info list"""
+
+		self.dp.multicastGmetric( 'MONARCH-HEARTBEAT', str( int( int( self.cur_time ) + int( self.timeoffset ) ) ) )
+
+		# Now let's spread the knowledge
+		#
+		for jobid, jobattrs in jobs.items():
+
+			gmetric_val = self.compileGmetricVal( jobid, jobattrs )
+
+			metric_increment = 0
+
+			for val in gmetric_val:
+				self.dp.multicastGmetric( 'MONARCH-JOB-' + jobid + '-' + str(metric_increment), val )
+				metric_increment = metric_increment + 1
+
+	def compileGmetricVal( self, jobid, jobattrs ):
+		"""Create a val string for gmetric of jobinfo"""
+
+		gval_lists = [ ]
+
+		mystr = None
+
+		val_list = { }
+
+		for val_name, val_value in jobattrs.items():
+
+			val_list_names_len	= len( string.join( val_list.keys() ) ) + len(val_list.keys())
+			val_list_vals_len	= len( string.join( val_list.values() ) ) + len(val_list.values())
+
+			if val_name == 'nodes' and jobattrs['status'] == 'R':
+
+				node_str = None
+
+				for node in val_value:
+
+					if node_str:
+						node_str = node_str + ';' + node
+					else:
+						node_str = node
+
+					if (val_list_names_len + len(val_name) ) + (val_list_vals_len + len(node_str) ) > METRIC_MAX_VAL_LEN:
+
+						val_list[ val_name ] = node_str
+						gval_lists.append( val_list )
+						val_list = { }
+						node_str = None
+
+				val_list[ val_name ] = node_str
+				gval_lists.append( val_list )
+				val_list = { }
+
+			elif val_value != '':
+
+				if (val_list_names_len + len(val_name) ) + (val_list_vals_len + len(str(val_value)) ) > METRIC_MAX_VAL_LEN:
+
+					gval_lists.append( val_list )
+					val_list = { }
+
+				val_list[ val_name ] = val_value
+
+		if len(val_list) > 0:
+			gval_lists.append( val_list )
+
+		str_list = [ ]
+
+		for val_list in gval_lists:
+
+			my_val_str = None
+
+			for val_name, val_value in val_list.items():
+
+				if my_val_str:
+
+					my_val_str = my_val_str + ' ' + val_name + '=' + val_value
+				else:
+					my_val_str = val_name + '=' + val_value
+
+			str_list.append( my_val_str )
+
+		return str_list
+
+def printTime( ):
+	"""Print current time/date in human readable format for log/debug"""
+
+	return time.strftime("%a, %d %b %Y %H:%M:%S")
+
+def debug_msg( level, msg ):
+	"""Print msg if at or above current debug level"""
+
+        if (DEBUG_LEVEL >= level):
+	                sys.stderr.write( msg + '\n' )
+
+def write_pidfile():
+
+	# Write pidfile if PIDFILE exists
+	if PIDFILE:
+		pid = os.getpid()
+
+		pidfile = open(PIDFILE, 'w')
+		pidfile.write(str(pid))
+		pidfile.close()
+
+def main():
+	"""Application start"""
+
+	global PBSQuery
+
+	if not processArgs( sys.argv[1:] ):
+		sys.exit( 1 )
+
+	if BATCH_API == 'pbs':
+
+		try:
+			from PBSQuery import PBSQuery, PBSError
+
+		except ImportError:
+
+			debug_msg( 0, "fatal error: BATCH_API set to 'pbs' but python module 'pbs_python' is not installed" )
+			sys.exit( 1 )
+
+		gather = PbsDataGatherer()
+
+	elif BATCH_API == 'sge':
+
+		pass
+		# import Babu's code here
+		#
+		#try:
+		#	import sge_drmaa
+		#
+		#except ImportError:
+		#
+		#	debug_msg( 0, "fatal error: BATCH_API set to 'sge' but python module 'sge_drmaa' is not installed' )
+		#	sys.exit( 1 )
+		#
+		#gather = SgeDataGatherer()
+
+	else:
+		debug_msg( 0, "fatal error: unknown BATCH_API '" + BATCH_API + "' is not supported" )
+		sys.exit( 1 )
+
+	if DAEMONIZE:
+		gather.daemon()
+	else:
+		gather.run()
+
+# wh00t? someone started me! :)
+#
+if __name__ == '__main__':
+	main()
Index: /tags/0.2/web/addons/job_monarch/conf.php
===================================================================
--- /tags/0.2/web/addons/job_monarch/conf.php	(revision 325)
+++ /tags/0.2/web/addons/job_monarch/conf.php	(revision 325)
@@ -0,0 +1,78 @@
+<?php
+// Show hosts in a jobview by default?
+//
+$default_showhosts = 1;
+
+// Stop displaying archive search results after SEARCH_RESULT_LIMIT
+//
+$SEARCH_RESULT_LIMIT = 20;
+
+// Show the column job attribute 'requested memory'?
+//
+$COLUMN_REQUESTED_MEMORY = 0;
+
+// Show the column job attribute 'queued' (since)?
+//
+$COLUMN_QUEUED = 1;
+
+// Show the column job attribute 'nodes' hostnames?
+//
+$COLUMN_NODES = 1;
+
+// Path to Ganglia's web frontend root
+//
+$GANGLIA_PATH = "/var/www/ganglia";
+
+// Format of how to display a date and time in human readable format
+//
+$DATETIME_FORMAT = "%a %d %b %Y %H:%M:%S";
+
+// Max size of small clusterimage
+// (250 pixels is same width as Ganglia's pie chart)
+//
+$SMALL_CLUSTERIMAGE_MAXWIDTH = 250;
+
+// The size of a single node in the small clusterimage
+//
+$SMALL_CLUSTERIMAGE_NODEWIDTH = 7;
+
+// Max size of small clusterimage
+// (250 pixels is same width as Ganglia's pie chart)
+//
+$BIG_CLUSTERIMAGE_MAXWIDTH = 250;
+
+// The size of a single node in the small clusterimage
+//
+$BIG_CLUSTERIMAGE_NODEWIDTH = 11;
+
+// Max size of small host image
+//
+$SMALL_HOSTIMAGE_MAXWIDTH = 450;
+
+// How to mark nodes with a job in clusterimage
+//
+$JOB_NODE_MARKING = "J";
+
+// XML Datasource for Job Monarch
+// by default localhost's gmetad
+// [syntax: <ip>:<port>]
+//
+$DATA_SOURCE = '127.0.0.1:8651';
+
+// Is there a jobarchive?
+//
+$JOB_ARCHIVE = 1;
+
+// Path to the job archive rrd files
+//
+$JOB_ARCHIVE_DIR = "/data/gfs/jobarch/rrds";
+
+// Location of the job archive database
+// [syntax: <ip>/<dbase>]
+//
+$JOB_ARCHIVE_DBASE = "127.0.0.1/jobarch";
+
+// Path to rrdtool binary
+//
+$RRDTOOL = "/usr/bin/rrdtool";
+?>
Index: /tags/0.2/web/addons/job_monarch/footer.php
===================================================================
--- /tags/0.2/web/addons/job_monarch/footer.php	(revision 325)
+++ /tags/0.2/web/addons/job_monarch/footer.php	(revision 325)
@@ -0,0 +1,44 @@
+<?php
+/*
+ *
+ * This file is part of Jobmonarch
+ *
+ * Copyright (C) 2006  Ramon Bastiaans
+ *
+ * Jobmonarch 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 2 of the License, or
+ * (at your option) any later version.
+ *
+ * Jobmonarch 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, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
+ *
+ * SVN $Id$
+ */
+
+$tpl = new TemplatePower( "templates/footer.tpl" );
+$tpl->prepare();
+$tpl->assign("webfrontend-version",$version["webfrontend"]);
+//$tpl->assign("togaweb-version", "0.1.1");
+//$tpl->assign("togaarch-version", "0.1.1");
+//$tpl->assign("togaplug-version", "0.1.1");
+
+if ($version["gmetad"]) {
+   $tpl->assign("webbackend-component", "gmetad");
+   $tpl->assign("webbackend-version",$version["gmetad"]);
+}
+elseif ($version["gmond"]) {
+   $tpl->assign("webbackend-component", "gmond");
+   $tpl->assign("webbackend-version", $version["gmond"]);
+}
+
+$tpl->assign("parsetime", sprintf("%.4f", $parsetime) . "s");
+
+$tpl->printToScreen();
+?>
Index: /tags/0.2/web/addons/job_monarch/graph.php
===================================================================
--- /tags/0.2/web/addons/job_monarch/graph.php	(revision 325)
+++ /tags/0.2/web/addons/job_monarch/graph.php	(revision 325)
@@ -0,0 +1,466 @@
+<?php
+/*
+ *
+ * This file is part of Jobmonarch
+ *
+ * Copyright (C) 2006  Ramon Bastiaans
+ *
+ * Jobmonarch 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 2 of the License, or
+ * (at your option) any later version.
+ *
+ * Jobmonarch 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, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
+ *
+ * SVN $Id$
+ */
+
+include_once "./libtoga.php";
+
+if ( !empty( $_GET ) ) {
+        extract( $_GET );
+}
+
+# Graph specific variables
+$size = escapeshellcmd( rawurldecode( $HTTP_GET_VARS["z"] ));
+$graph = escapeshellcmd( rawurldecode( $HTTP_GET_VARS["g"] ));
+$grid = escapeshellcmd( rawurldecode( $HTTP_GET_VARS["G"] ));
+$self = escapeshellcmd( rawurldecode( $HTTP_GET_VARS["me"] ));
+$max = escapeshellcmd( rawurldecode( $HTTP_GET_VARS["x"] ));
+$min = escapeshellcmd( rawurldecode( $HTTP_GET_VARS["n"] ));
+$value = escapeshellcmd( rawurldecode( $HTTP_GET_VARS["v"] ));
+$load_color = escapeshellcmd( rawurldecode( $HTTP_GET_VARS["l"] ));
+$vlabel = escapeshellcmd( rawurldecode( $HTTP_GET_VARS["vl"] ));
+$sourcetime = escapeshellcmd($HTTP_GET_VARS["st"]);
+
+$cluster = $c;
+$metricname = ($g) ? $g : $m;
+$hostname = $h;
+
+# Assumes we have a $start variable (set in get_context.php).
+if ($size == "small") {
+	$height = 40;
+	$width = 130;
+} else if ($size == "medium") {
+	$height = 75;
+	$width = 300;
+} else {
+	$height = 100;
+	$width = 400;
+}
+
+if($command) {
+      $command = '';
+}
+
+//printf( "cluster = %s hostname = %s metric = %s\n", $cluster, $hostname, $metricname );
+
+$trd = new TarchRrdGraph( $cluster, $hostname );
+//$rrd_files = $trd->getRrdFiles( $metricname, $start, $stop );
+
+//print_r( $rrd_files );
+
+$graph = $metricname;
+
+if (isset($graph)) {
+	$rrd_dirs = $trd->getRrdDirs( $period_start, $period_stop );
+	$series = '';
+
+	if($graph == "cpu_report") {
+		$style = "CPU";
+		//printf("ik doe die shit!\n");
+
+		$upper_limit = "--upper-limit 100 --rigid";
+		$lower_limit = "--lower-limit 0";
+
+		$vertical_label = "--vertical-label Percent ";
+
+		$def_nr = 0;
+
+		foreach( $rrd_dirs as $rrd_dir ) {
+
+			if( $def_nr == 0 ) {
+
+				$user_str = ":'User CPU'";
+				$nice_str = ":'Nice CPU'";
+				$system_str = ":'System CPU'";
+				$wio_str = ":'WAIT CPU'";
+				$idle_str = ":'Idle CPU'";
+			} else {
+
+				$user_str = "";
+				$nice_str = "";
+				$system_str = "";
+				$wio_str = "";
+				$idle_str = "";
+			}
+
+			$series .= "DEF:'cpu_user${def_nr}'='${rrd_dir}/cpu_user.rrd':'sum':AVERAGE "
+				."DEF:'cpu_nice${def_nr}'='${rrd_dir}/cpu_nice.rrd':'sum':AVERAGE "
+				."DEF:'cpu_system${def_nr}'='${rrd_dir}/cpu_system.rrd':'sum':AVERAGE "
+				."DEF:'cpu_idle${def_nr}'='${rrd_dir}/cpu_idle.rrd':'sum':AVERAGE "
+				."AREA:'cpu_user${def_nr}'#${cpu_user_color}${user_str} "
+				."STACK:'cpu_nice${def_nr}'#${cpu_nice_color}${nice_str} "
+				."STACK:'cpu_system${def_nr}'#${cpu_system_color}${system_str} ";
+
+			if (file_exists("$rrd_dir/cpu_wio.rrd")) {
+				$series .= "DEF:'cpu_wio${def_nr}'='${rrd_dir}/cpu_wio.rrd':'sum':AVERAGE "
+					."STACK:'cpu_wio${def_nr}'#${cpu_wio_color}${wio_str} ";
+			}
+
+			$series .= "STACK:'cpu_idle${def_nr}'#${cpu_idle_color}${idle_str} ";
+
+			$def_nr++;
+		}
+
+	} else if ($graph == "mem_report") {
+		$style = "Memory";
+
+		$lower_limit = "--lower-limit 0 --rigid";
+		$extras = "--base 1024";
+		$vertical_label = "--vertical-label Bytes";
+
+		$def_nr = 0;
+
+		foreach( $rrd_dirs as $rrd_dir ) {
+
+			if( $def_nr == 0 ) {
+
+				$memuse_str = ":'Memory Used'";
+				$memshared_str = ":'Memory Shared'";
+				$memcached_str = ":'Memory Cached'";
+				$membuff_str = ":'Memory Buffered'";
+				$memswap_str = ":'Memory Swapped'";
+				$total_str = ":'Total In-Core Memory'";
+			} else {
+
+				$memuse_str = "";
+				$memshared_str = "";
+				$memcached_str = "";
+				$membuff_str = "";
+				$memswap_str = "";
+				$total_str = "";
+			}
+
+			$series .= "DEF:'mem_total${def_nr}'='${rrd_dir}/mem_total.rrd':'sum':AVERAGE "
+				."CDEF:'bmem_total${def_nr}'=mem_total${def_nr},1024,* "
+				."DEF:'mem_shared${def_nr}'='${rrd_dir}/mem_shared.rrd':'sum':AVERAGE "
+				."CDEF:'bmem_shared${def_nr}'=mem_shared${def_nr},1024,* "
+				."DEF:'mem_free${def_nr}'='${rrd_dir}/mem_free.rrd':'sum':AVERAGE "
+				."CDEF:'bmem_free${def_nr}'=mem_free${def_nr},1024,* "
+				."DEF:'mem_cached${def_nr}'='${rrd_dir}/mem_cached.rrd':'sum':AVERAGE "
+				."CDEF:'bmem_cached${def_nr}'=mem_cached${def_nr},1024,* "
+				."DEF:'mem_buffers${def_nr}'='${rrd_dir}/mem_buffers.rrd':'sum':AVERAGE "
+				."CDEF:'bmem_buffers${def_nr}'=mem_buffers${def_nr},1024,* "
+				."CDEF:'bmem_used${def_nr}'='bmem_total${def_nr}','bmem_shared${def_nr}',-,'bmem_free${def_nr}',-,'bmem_cached${def_nr}',-,'bmem_buffers${def_nr}',- "
+				."AREA:'bmem_used${def_nr}'#${mem_used_color}${memuse_str} "
+				."STACK:'bmem_shared${def_nr}'#${mem_shared_color}${memshared_str} "
+				."STACK:'bmem_cached${def_nr}'#${mem_cached_color}${memcached_str} "
+				."STACK:'bmem_buffers${def_nr}'#${mem_buffered_color}${membuff_str} ";
+
+			if (file_exists("$rrd_dir/swap_total.rrd")) {
+				$series .= "DEF:'swap_total${def_nr}'='${rrd_dir}/swap_total.rrd':'sum':AVERAGE "
+					."DEF:'swap_free${def_nr}'='${rrd_dir}/swap_free.rrd':'sum':AVERAGE "
+					."CDEF:'bmem_swapped${def_nr}'='swap_total${def_nr}','swap_free${def_nr}',-,1024,* "
+					."STACK:'bmem_swapped${def_nr}'#${mem_swapped_color}${memswap_str} ";
+			}
+
+			$series .= "LINE2:'bmem_total${def_nr}'#${cpu_num_color}${total_str} ";
+
+			$def_nr++;
+		}
+
+	} else if ($graph == "load_report") {
+		$style = "Load";
+
+		$lower_limit = "--lower-limit 0 --rigid";
+		$vertical_label = "--vertical-label 'Load/Procs'";
+
+		$def_nr = 0;
+
+		foreach( $rrd_dirs as $rrd_dir ) {
+
+			if( $def_nr == 0 ) {
+
+				$load_str = ":'1-min Load'";
+				$cpu_str = ":'CPUs'";
+				$run_str = ":'Running Processes'";
+			} else {
+				$load_str = "";
+				$cpu_str = "";
+				$run_str = "";
+			}
+
+			$series .= "DEF:'load_one${def_nr}'='${rrd_dir}/load_one.rrd':'sum':AVERAGE "
+				."DEF:'proc_run${def_nr}'='${rrd_dir}/proc_run.rrd':'sum':AVERAGE "
+				."DEF:'cpu_num${def_nr}'='${rrd_dir}/cpu_num.rrd':'sum':AVERAGE ";
+			$series .="AREA:'load_one${def_nr}'#${load_one_color}${load_str} ";
+			$series .="LINE2:'cpu_num${def_nr}'#${cpu_num_color}${cpu_str} ";
+			$series .="LINE2:'proc_run${def_nr}'#${proc_run_color}${run_str} ";
+
+			$def_nr++;
+		}
+
+	} else if ($graph == "network_report") {
+		$style = "Network";
+
+		$lower_limit = "--lower-limit 0 --rigid";
+		$extras = "--base 1024";
+		$vertical_label = "--vertical-label 'Bytes/sec'";
+
+		$def_nr = 0;
+
+		foreach( $rrd_dirs as $rrd_dir ) {
+
+			if( $def_nr == 0 ) {
+
+				$in_str = ":'In'";
+				$out_str = ":'Out'";
+			} else {
+
+				$in_str = "";
+				$out_str = "";
+			}
+
+			$series .= "DEF:'bytes_in${def_nr}'='${rrd_dir}/bytes_in.rrd':'sum':AVERAGE "
+				."DEF:'bytes_out${def_nr}'='${rrd_dir}/bytes_out.rrd':'sum':AVERAGE "
+				."LINE2:'bytes_in${def_nr}'#${mem_cached_color}${in_str} "
+				."LINE2:'bytes_out${def_nr}'#${mem_used_color}${out_str} ";
+
+			$def_nr++;
+		}
+
+	} else if ($graph == "packet_report") {
+		$style = "Packets";
+
+		$lower_limit = "--lower-limit 0 --rigid";
+		$extras = "--base 1024";
+		$vertical_label = "--vertical-label 'Packets/sec'";
+
+		$def_nr = 0;
+
+		foreach( $rrd_dirs as $rrd_dir ) {
+
+			if( $def_nr == 0 ) {
+
+				$in_str = ":'In'";
+				$out_str = ":'Out'";
+			} else {
+
+				$in_str = "";
+				$out_str = "";
+			}
+
+			$series .= "DEF:'bytes_in${def_nr}'='${rrd_dir}/pkts_in.rrd':'sum':AVERAGE "
+				."DEF:'bytes_out${def_nr}'='${rrd_dir}/pkts_out.rrd':'sum':AVERAGE "
+				."LINE2:'bytes_in${def_nr}'#${mem_cached_color}${in_str} "
+				."LINE2:'bytes_out${def_nr}'#${mem_used_color}${out_str} ";
+
+			$def_nr++;
+		}
+
+	} else {
+		/* Custom graph */
+		$style = "";
+
+		$subtitle = $metricname;
+		if ($context == "host") {
+			if ($size == "small")
+				$prefix = $metricname;
+			else
+				$prefix = $hostname;
+
+			$value = $value>1000 ? number_format($value) : number_format($value, 2);
+		}
+
+		//if ($range=="job") {
+		//	$hrs = intval( -$jobrange / 3600 );
+		//	$subtitle = "$prefix last ${hrs}h (now $value)";
+		//} else
+		//	$subtitle = "$prefix last $range (now $value)";
+
+		if (is_numeric($max))
+			$upper_limit = "--upper-limit '$max' ";
+		if (is_numeric($min))
+			$lower_limit ="--lower-limit '$min' ";
+
+		if ($vlabel)
+			$vertical_label = "--vertical-label '$vlabel'";
+		else {
+			if ($upper_limit or $lower_limit) {
+				$max = $max>1000 ? number_format($max) : number_format($max, 2);
+				$min = $min>0 ? number_format($min,2) : $min;
+
+				$vertical_label ="--vertical-label '$min - $max' ";
+			}
+		}
+
+		$def_nr = 0;
+
+		foreach( $rrd_dirs as $rrd_dir ) {
+
+			if( $def_nr == 0 ) {
+				$title_str = ":'${subtitle}'";
+			} else {
+				$title_str = "";
+			}
+
+			$rrd_file = "$rrd_dir/$metricname.rrd";
+			$series .= "DEF:'sum${def_nr}'='$rrd_file':'sum':AVERAGE "
+				."AREA:'sum${def_nr}'#${default_metric_color}${title_str} ";
+
+			$def_nr++;
+		}
+
+	}
+	if( $series != '' ) {
+		if ($job_start)
+			$series .= "VRULE:${job_start}#${jobstart_color} ";
+		if ($job_stop)
+			$series .= "VRULE:${job_stop}#${jobstart_color} ";
+	}
+}
+
+//$title = "$hostname $style $metricname";
+$title = "$hostname";
+
+//# Set the graph title.
+//if($context == "meta") {
+//	$title = "$self $meta_designator $style last $range";
+//} else if ($context == "grid") {
+//	$title = "$grid $meta_designator $style last $range";
+//} else if ($context == "cluster") {
+//	$title = "$clustername $style last $range";
+//} else {
+//	if ($size == "small") {
+//		# Value for this graph define a background color.
+//		if (!$load_color) $load_color = "ffffff";
+//			$background = "--color BACK#'$load_color'";
+
+//		$title = $hostname;
+//	} else {
+//		if ($style)
+//			$title = "$hostname $style last $range";
+//		else
+//			$title = $metricname;
+//	}
+//}
+
+function determineXGrid( $p_start, $p_stop ) {
+
+	$period = intval( $p_stop - $p_start );
+
+	// Syntax: <minor_grid_lines_time_declr>:<major_grid_lines_time_declr>:<labels_time_declr>:<offset>:<format>
+	//
+	// Where each <*time_declr*> = <time_type>:<time_interval>
+
+	//$my_lines1 = intval( $period / 3.0 );
+	//$my_lines2 = intval( $period / 6.0 );
+
+	//$my_grid = "SECOND:$my_lines2:SECOND:$my_lines1:SECOND:$my_lines1:0:%R";
+
+	//return "--x-grid $my_grid";
+
+	// Less than 1 minute
+	if( $period < 60 ) {
+
+		$tm_formt = "%X";
+		$my_grid = "SECOND:15:SECOND:30:SECOND:30:0:$tm_formt";
+
+	// Less than 10 minutes
+	} else if( $period < 600 ) {
+
+		$tm_formt = "%R";
+		$my_grid = "MINUTE:1:MINUTE:3:MINUTE:3:0:$tm_formt";
+
+	// Less than 1 hour
+	} else if( $period < 3600 ) {
+
+		$tm_formt = "%R";
+		$my_grid = "MINUTE:5:MINUTE:15:MINUTE:15:0:$tm_formt";
+
+	// Less than 15 hour
+	} else if( $period < 3600 ) {
+
+		$tm_formt = "%R";
+		$my_grid = "HOUR:1:HOUR:2:HOUR:2:0:$tm_formt";
+
+	// Less than 1 day
+	//
+	} else if( $period < 86400 ) {
+
+		$tm_formt = "%R";
+		$my_grid = "HOUR:2:HOUR:5:HOUR:5:0:$tm_formt";
+
+	// Less than 15 days
+	//
+	} else if( $period < 1296000 ) {
+
+		$tm_formt = "%e-%m";
+		$my_grid = "HOUR:1:DAY:3:DAY:3:0:'$tm_formt'";
+		
+	// Less than 30 days (a month)
+	//
+	} else if( $period < 2592000 ) {
+
+		$tm_formt = "%e-%m";
+		$my_grid = "DAY:5:DAY:10:DAY:10:0:'$tm_formt'";
+	}
+
+	if( isset( $my_grid ) ) {
+
+		$ret_str = "--x-grid $my_grid";
+		return array($ret_str,$tm_formt);
+
+	} else {
+		return array( "", "" );
+	}
+}
+
+#list( $xgrid, $t_format ) = determineXGrid( $period_start, $period_stop );
+
+#if( $t_format != "" ) {
+#	$prnt_start = strftime( $t_format, $period_start );
+#	$prnt_stop = strftime( $t_format, $period_stop );
+#	$series .= " COMMENT:'     Timescale $prnt_start - $prnt_stop' ";
+#}
+
+$lower_limit = "--lower-limit 0";
+
+#
+# Generate the rrdtool graph command.
+#
+#$command = RRDTOOL . " graph - --start $period_start --end $period_stop ".
+#	"--width $width --height $height $upper_limit $lower_limit ".
+#	"--title '$title' $vertical_label $extras $background $xgrid ".
+#	$series;
+$command = RRDTOOL . " graph - --start $period_start --end $period_stop ".
+	"--width $width --height $height $lower_limit ".
+	"--title '$title' $extras $background ".
+	$series;
+
+$debug=0;
+
+# Did we generate a command?   Run it.
+if($command) {
+	/*Make sure the image is not cached*/
+	header ("Expires: Mon, 26 Jul 1997 05:00:00 GMT");   // Date in the past
+	header ("Last-Modified: " . gmdate("D, d M Y H:i:s") . " GMT"); // always modified
+	header ("Cache-Control: no-cache, must-revalidate");   // HTTP/1.1
+	header ("Pragma: no-cache");                     // HTTP/1.0
+	if ($debug) {
+		header ("Content-type: text/html");
+		print "$command\n\n\n\n\n";
+	} else {
+		header ("Content-type: image/gif");
+		passthru($command);
+	}
+}
+?>
Index: /tags/0.2/web/addons/job_monarch/header.php
===================================================================
--- /tags/0.2/web/addons/job_monarch/header.php	(revision 325)
+++ /tags/0.2/web/addons/job_monarch/header.php	(revision 325)
@@ -0,0 +1,174 @@
+<?php
+/*
+ *
+ * This file is part of Jobmonarch
+ *
+ * Copyright (C) 2006  Ramon Bastiaans
+ *
+ * Jobmonarch 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 2 of the License, or
+ * (at your option) any later version.
+ *
+ * Jobmonarch 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, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
+ *
+ * SVN $Id$
+ */
+
+# Check if this context is private.
+include_once "$GANGLIA_PATH/auth.php";
+checkcontrol();
+checkprivate();
+
+if ( $context == "control" && $controlroom < 0 )
+      $header = "header-nobanner";
+else
+      $header = "header";
+
+$tpl = new TemplatePower( "templates/header.tpl" );
+$tpl->prepare();
+
+# Maintain our path through the grid tree.
+$me = $self . "@" . $grid[$self][AUTHORITY];
+if ($initgrid)
+   {
+      $gridstack = array();
+      $gridstack[] = $me;
+   }
+else if ($gridwalk=="fwd")
+   {
+      # push our info on gridstack, format is "name@url>name2@url".
+      if (end($gridstack) != $me)
+         {
+            $gridstack[] = $me;
+         }
+   }
+else if ($gridwalk=="back")
+   {
+      # pop a single grid off stack.
+      if (end($gridstack) != $me)
+         {
+            array_pop($gridstack);
+         }
+   }
+$gridstack_str = join(">", $gridstack);
+$gridstack_url = rawurlencode($gridstack_str);
+
+if ($initgrid or $gridwalk)
+   {
+      # Use cookie so we dont have to pass gridstack around within this site.
+      # Cookie values are automatically urlencoded. Expires in a day.
+      setcookie("gs", $gridstack_str, time() + 86400);
+   }
+
+# Invariant: back pointer is second-to-last element of gridstack. Grid stack never
+# has duplicate entries.
+list($parentgrid, $parentlink) = explode("@", $gridstack[count($gridstack)-2]);
+
+# Setup a redirect to a remote server if you choose a grid from pulldown menu. Tell
+# destination server that we're walking foward in the grid tree.
+if (strstr($clustername, "http://"))
+   {
+      $tpl->assign("refresh", "0");
+      $tpl->assign("redirect", ";URL=$clustername?gw=fwd&gs=$gridstack_url");
+      echo "<h2>Redirecting, please wait...</h2>";
+      $tpl->printToScreen();
+      exit;
+   }
+$tpl->assign("refresh", $default_refresh);
+
+$tpl->assign( "date", date("r"));
+$tpl->assign( "page_title", $title );
+
+# The page to go to when "Get Fresh Data" is pressed.
+if (isset($page))
+      $tpl->assign("page",$page);
+else
+      $tpl->assign("page","./");
+
+# Templated Logo image
+$tpl->assign("images","./templates/$template_name/images");
+
+#
+# Used when making graphs via graph.php. Included in most URLs
+#
+$sort_url=rawurlencode($sort);
+$get_metric_string = "m=$metric&r=$range&s=$sort_url&hc=$hostcols";
+if ($jobrange and $jobstart)
+        $get_metric_string .= "&jr=$jobrange&js=$jobstart";
+
+# Set the Alternate view link.
+$cluster_url=rawurlencode($clustername);
+$node_url=rawurlencode($hostname);
+
+# Make some information available to templates.
+$tpl->assign("cluster_url", $cluster_url);
+
+# Build the node_menu
+$node_menu = "";
+
+if ($parentgrid)
+   {
+      $node_menu .= "<B><A HREF=\"$parentlink?gw=back&gs=$gridstack_url\">".
+         "$parentgrid $meta_designator</A></B> ";
+      $node_menu .= "<B>&gt;</B>\n";
+   }
+
+# Show grid.
+$mygrid =  ($self == "unspecified") ? "" : $self;
+$node_menu .= "<B><A HREF=\"./?$get_metric_string\">$mygrid $meta_designator</A> ";
+$node_menu .= "<B>&gt;</B>\n";
+
+if ($physical)
+   $node_menu .= hiddenvar("p", $physical);
+
+if ( $clustername )
+   {
+      $url = rawurlencode($clustername);
+      $node_menu .= "<B><A HREF=\"./?c=$url&$get_metric_string\">$clustername</A></B> ";
+      //$node_menu .= "<B>&gt;</B>\n";
+      $node_menu .= hiddenvar("c", $clustername);
+   }
+else
+   {
+      # No cluster has been specified, so drop in a list
+      $node_menu .= "<SELECT NAME=\"c\" OnChange=\"ganglia_form.submit();\">\n";
+      $node_menu .= "<OPTION VALUE=\"\">--Choose a Source\n";
+      ksort($grid);
+      foreach( $grid as $k => $v )
+         {
+            if ($k==$self) continue;
+            if ($v[GRID])
+               {
+                  $url = $v[AUTHORITY];
+                  $node_menu .="<OPTION VALUE=\"$url\">$k $meta_designator\n";
+               }
+            else
+               {
+                  $url = rawurlencode($k);
+                  $node_menu .="<OPTION VALUE=\"$url\">$k\n";
+               }
+         }
+      $node_menu .= "</SELECT>\n";
+   }
+
+$tpl->assign("node_menu", $node_menu);
+
+# Make sure that no data is cached..
+header ("Expires: Mon, 26 Jul 1997 05:00:00 GMT");    # Date in the past
+header ("Last-Modified: " . gmdate("D, d M Y H:i:s") . " GMT"); # always modified
+header ("Cache-Control: no-cache, must-revalidate");  # HTTP/1.1
+header ("Pragma: no-cache");                          # HTTP/1.0
+
+$tpl->printToScreen();
+
+
+
+?>
Index: /tags/0.2/web/addons/job_monarch/host_view.php
===================================================================
--- /tags/0.2/web/addons/job_monarch/host_view.php	(revision 325)
+++ /tags/0.2/web/addons/job_monarch/host_view.php	(revision 325)
@@ -0,0 +1,194 @@
+<?php
+/*
+ *
+ * This file is part of Jobmonarch
+ *
+ * Copyright (C) 2006  Ramon Bastiaans
+ *
+ * Jobmonarch 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 2 of the License, or
+ * (at your option) any later version.
+ *
+ * Jobmonarch 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, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
+ *
+ * SVN $Id$
+ */
+
+include_once "./libtoga.php";
+
+//$tpl = new TemplatePower( "templates/host_view.tpl" );
+//$tpl->assignInclude("extra", "templates/host_extra.tpl");
+//$tpl->prepare();
+
+function datetimeToEpoch( $datetime ) {
+
+        //printf("datetime = %s\n", $datetime );
+        $datetime_fields = explode( ' ', $datetime );
+
+        $date = $datetime_fields[0];
+        $time = $datetime_fields[1];
+
+        $date_fields = explode( '-', $date );
+
+        $days = $date_fields[0];
+        $months = $date_fields[1];
+        $years = $date_fields[2];
+
+        //printf( "days = %s months = %s years = %s\n", $days, $months, $years );
+
+        $time_fields = explode( ':', $time );
+
+        $hours = $time_fields[0];
+        $minutes = $time_fields[1];
+        $seconds = $time_fields[2];
+
+        //printf( "hours = %s minutes = %s seconds = %s\n", $hours, $minutes, $seconds );
+
+        $timestamp = mktime( $hours, $minutes, $seconds, $months, $days, $years );
+
+        //printf( "timestamp = %s\n", $timestamp );
+
+        return $timestamp;
+}
+
+function makeHostView() {
+
+	global $tpl, $metrics, $clustername, $hostname;
+	global $cluster_ul, $hosts_up, $get_metric_string;
+	global $cluster, $period_start, $period_stop;
+	global $job_start, $job_stop;
+
+	$metrics = $metrics[$hostname];
+	//print_r( $metrics );
+	$hosts_up = $hosts_up[$hostname];
+	//print_r( $hosts_up );
+
+	$tpl->assign("cluster", $clustername);
+	$tpl->assign("host", $hostname);
+	$tpl->assign("node_image", "../../".node_image($metrics));
+	$tpl->assign("sort",$sort);
+	$tpl->assign("range",$range);
+
+	if( !is_numeric( $period_start ) ) {
+		$period_start = datetimeToEpoch( $period_start );
+	}
+	if( !is_numeric( $period_stop ) ) {
+		$period_stop = datetimeToEpoch( $period_stop );
+	}
+
+	if($hosts_up)
+	      $tpl->assign("node_msg", "This host is up and running."); 
+	else
+	      $tpl->assign("node_msg", "This host is down."); 
+
+	$cluster_url=rawurlencode($clustername);
+	$tpl->assign("cluster_url", $cluster_url);
+	$tpl->assign("graphargs", "h=$hostname&$get_metric_string&st=$cluster[LOCALTIME]&job_start=$job_start&job_stop=$job_stop&period_start=$period_start&period_stop=$period_stop");
+
+	# For the node view link.
+	$tpl->assign("node_view","./?p=2&c=$cluster_url&h=$hostname");
+
+	//# No reason to go on if this node is down.
+	//if ($hosts_down)
+	//   {
+	//      $tpl->printToScreen();
+	//      return;
+	//   }
+
+	$tpl->assign("ip", $hosts_up[IP]);
+
+	foreach ($metrics as $name => $v)
+	   {
+	       if ($v[TYPE] == "string" or $v[TYPE]=="timestamp" or $always_timestamp[$name])
+		  {
+		     # Long gmetric name/values will disrupt the display here.
+		     if ($v[SOURCE] == "gmond") $s_metrics[$name] = $v;
+		  }
+	       elseif ($v[SLOPE] == "zero" or $always_constant[$name])
+		  {
+		     $c_metrics[$name] = $v;
+		  }
+	       else if ($reports[$metric])
+		  continue;
+	       else
+		  {
+		     $graphargs = "c=$cluster_url&h=$hostname&v=$v[VAL]&m=$name"
+		       ."&z=medium&st=$cluster[LOCALTIME]&job_start=$job_start&job_stop=$job_stop&period_start=$period_start&period_stop=$period_stop";
+		     # Adding units to graph 2003 by Jason Smith <smithj4@bnl.gov>.
+		     if ($v[UNITS]) {
+			$encodeUnits = rawurlencode($v[UNITS]);
+			$graphargs .= "&vl=$encodeUnits";
+		     }
+		     $g_metrics[$name][graph] = $graphargs;
+          }
+	   }
+	# Add the uptime metric for this host. Cannot be done in ganglia.php,
+	# since it requires a fully-parsed XML tree. The classic contructor problem.
+	$s_metrics[uptime][TYPE] = "string";
+	$s_metrics[uptime][VAL] = uptime($cluster[LOCALTIME] - $metrics[boottime][VAL]);
+
+	# Add the gmond started timestamps & last reported time (in uptime format) from
+	# the HOST tag:
+	$s_metrics[gmond_started][TYPE] = "timestamp";
+	$s_metrics[gmond_started][VAL] = $hosts_up[GMOND_STARTED];
+	$s_metrics[last_reported][TYPE] = "string";
+	$s_metrics[last_reported][VAL] = uptime($cluster[LOCALTIME] - $hosts_up[REPORTED]);
+
+	# Show string metrics
+	if (is_array($s_metrics))
+	   {
+	      ksort($s_metrics);
+	      foreach ($s_metrics as $name => $v )
+	     {
+		$tpl->newBlock("string_metric_info");
+		$tpl->assign("name", $name);
+		if( $v[TYPE]=="timestamp" or $always_timestamp[$name])
+		   {
+		      $tpl->assign("value", date("r", $v[VAL]));
+		   }
+		else
+		   {
+		      $tpl->assign("value", "$v[VAL] $v[UNITS]");
+		   }
+	     }
+	   }
+
+	# Show constant metrics.
+	if (is_array($c_metrics))
+	   {
+	      ksort($c_metrics);
+	      foreach ($c_metrics as $name => $v )
+	     {
+		$tpl->newBlock("const_metric_info");
+		$tpl->assign("name", $name);
+		$tpl->assign("value", "$v[VAL] $v[UNITS]");
+	     }
+	   }
+
+	# Show graphs.
+	if (is_array($g_metrics))
+	   {
+	      ksort($g_metrics);
+
+	      $i = 0;
+	      foreach ( $g_metrics as $name => $v )
+		 {
+		    $tpl->newBlock("vol_metric_info");
+		    $tpl->assign("graphargs", $v[graph]);
+		    $tpl->assign("alt", "$hostname $name");
+		    if($i++ %2)
+		       $tpl->assign("br", "<BR>");
+		 }
+	   }
+}
+
+	//$tpl->printToScreen();
+?>
Index: /tags/0.2/web/addons/job_monarch/image.php
===================================================================
--- /tags/0.2/web/addons/job_monarch/image.php	(revision 325)
+++ /tags/0.2/web/addons/job_monarch/image.php	(revision 325)
@@ -0,0 +1,130 @@
+<?php
+/*
+ *
+ * This file is part of Jobmonarch
+ *
+ * Copyright (C) 2006  Ramon Bastiaans
+ *
+ * Jobmonarch 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 2 of the License, or
+ * (at your option) any later version.
+ *
+ * Jobmonarch 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, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
+ *
+ * SVN $Id$
+ */
+
+include_once "./libtoga.php";
+
+if ( !empty( $_GET ) ) {
+        extract( $_GET );
+}
+
+$httpvars = new HTTPVariables( $HTTP_GET_VARS, $_GET );
+$view = $httpvars->getHttpVar( "view" );
+$clustername = $httpvars->getClusterName();
+
+//printf("clustername = %s\n", $clustername );
+if( isset($id) && ($id!='')) $filter[id]=$id;
+if( isset($state) && ($state!='')) $filter[state]=$state;
+if( isset($user) && ($user!='')) $filter[user]=$user;
+if( isset($queue) && ($queue!='')) $filter[queue]=$queue;
+
+function drawHostImage() {
+
+	global $clustername, $hostname;
+
+	$data_gatherer = new DataGatherer( $clustername );
+	$data_gatherer->parseXML();
+
+	if( $data_gatherer->isJobmonRunning() )
+		$ic = new HostImage( $data_gatherer, $clustername, $hostname );
+	else
+		$ic = new EmptyImage();
+
+	$ic->draw();
+}
+
+function drawSmallClusterImage() {
+
+	global $clustername;
+
+	$data_gatherer = new DataGatherer( $clustername );
+	$data_gatherer->parseXML();
+
+	if( $data_gatherer->isJobmonRunning() ) {
+		$ic = new ClusterImage( $clustername );
+		$ic->setSmall();
+	} else {
+		$ic = new EmptyImage();
+	}
+
+	$ic->draw();
+}
+
+function drawBigClusterImage() {
+
+	global $filter, $clustername;
+
+	$ic = new ClusterImage( $clustername );
+	$ic->setBig();
+
+	if( isset( $filter ) ) {
+		foreach( $filter as $filtername=>$filtervalue ) {
+			//printf("filter %s,%s\n", $filtername, $filtervalue);
+			switch( $filtername ) {
+
+				case "id":
+					$ic->setFilter( 'jobid', $filtervalue );
+					break;
+				case "user":
+					$ic->setFilter( 'owner', $filtervalue);
+					break;
+				case "queue":
+					$ic->setFilter( 'queue', $filtervalue);
+					break;
+				case "state":
+					$ic->setFilter( 'status', $filtervalue);
+					break;
+				default:
+					break;
+			}
+		}
+	}
+	$ic->draw();
+}
+
+switch( $view ) {
+
+	case "small-clusterimage":
+
+		drawSmallClusterImage();
+		
+		break;
+
+	case "big-clusterimage":
+
+		drawBigClusterImage();
+	
+		break;
+
+	case "hostimage":
+
+		drawHostImage();
+	
+		break;
+
+	default:
+
+		break;
+}
+
+?>
Index: /tags/0.2/web/addons/job_monarch/index.php
===================================================================
--- /tags/0.2/web/addons/job_monarch/index.php	(revision 325)
+++ /tags/0.2/web/addons/job_monarch/index.php	(revision 325)
@@ -0,0 +1,452 @@
+<?php
+/*
+ *
+ * This file is part of Jobmonarch
+ *
+ * Copyright (C) 2006  Ramon Bastiaans
+ *
+ * Jobmonarch 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 2 of the License, or
+ * (at your option) any later version.
+ *
+ * Jobmonarch 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, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
+ *
+ * SVN $Id$
+ */
+
+if ( !empty( $_GET ) ) {
+	extract( $_GET );
+}
+
+$my_dir = getcwd();
+
+include_once "./libtoga.php";
+
+global $GANGLIA_PATH;
+chdir( $GANGLIA_PATH );
+
+include_once "./class.TemplatePower.inc.php";
+chdir( $my_dir );
+
+$httpvars = new HTTPVariables( $HTTP_GET_VARS, $_GET );
+$clustername = $httpvars->getClusterName();
+$view = $httpvars->getHttpVar( "view" );
+
+$filter = array();
+
+if( !isset($view) ) $view = "overview";
+if( !isset($sortorder) ) $sortorder = "asc";
+if( !isset($sortby) ) $sortby = "id";
+
+if( isset( $filterorder ) && ($filterorder!='') ) {
+	$myfilter_fields = explode( ",", $filterorder );
+} else {
+	if( isset($queue) && ($queue!='')) $filter[queue]=$queue;
+	if( isset($state) && ($state!='')) $filter[state]=$state;
+	if( isset($user) && ($user!='')) $filter[user]=$user;
+	if( isset($id) && ($id!='')) $filter[id]=$id;
+}
+
+// Fill filter array in order they were picked by user
+if( isset($myfilter_fields) ) {
+
+	foreach( $myfilter_fields as $myfilter ) {
+
+		switch( $myfilter ) {
+
+			case "queue":
+				$filter[queue]=$queue;
+				break;
+			case "state":
+				$filter[state]=$state;
+				break;
+			case "user":
+				$filter[user]=$user;
+				break;
+			case "id":
+				$filter[id]=$id;
+				break;
+		}
+	}
+}
+
+//if( isset($queue) && ($queue!='')) $filter[queue]=$queue;
+//if( isset($state) && ($state!='')) $filter[state]=$state;
+//if( isset($user) && ($user!='')) $filter[user]=$user;
+//if( isset($id) && ($id!='')) $filter[id]=$id;
+
+function epochToDatetime( $epoch ) {
+
+        return strftime( "%d-%m-%Y %H:%M:%S", $epoch );
+}
+
+function makeHeader( $page_call ) {
+
+	global $tpl, $grid, $context, $initgrid;
+	global $jobrange, $jobstart, $title;
+	global $page, $gridwalk, $clustername;
+	global $parentgrid, $physical, $hostname;
+	global $self, $filter, $cluster_url, $get_metric_string;
+	global $metrics, $reports, $m, $default_metric;
+	global $default_refresh, $filterorder, $view;
+	global $JOB_ARCHIVE, $period_start, $period_stop, $h, $id;
+	global $job_start, $job_stop;
+	
+	if( isset($default_metric) and !isset($m) )
+		$metricname = $default_metric;
+	else
+		if( isset( $m ) )
+			$metricname = $m;
+		else
+			$metricname = "load_one";
+
+	$header = "header";
+
+	# Maintain our path through the grid tree.
+	$me = $self . "@" . $grid[$self][AUTHORITY];
+
+	$gridstack = array();
+	$gridstack[] = $me;
+
+	if ($gridwalk=="fwd") {
+		# push our info on gridstack, format is "name@url>name2@url".
+		if (end($gridstack) != $me) {
+			$gridstack[] = $me;
+		}
+	} else if ($gridwalk=="back") {
+		# pop a single grid off stack.
+		if (end($gridstack) != $me) {
+			array_pop($gridstack);
+		}
+	}
+
+	$gridstack_str = join(">", $gridstack);
+	$gridstack_url = rawurlencode($gridstack_str);
+
+	if ($initgrid or $gridwalk) {
+		# Use cookie so we dont have to pass gridstack around within this site.
+		# Cookie values are automatically urlencoded. Expires in a day.
+		setcookie("gs", $gridstack_str, time() + 86400);
+	}
+
+	# Invariant: back pointer is second-to-last element of gridstack. Grid stack never
+	# has duplicate entries.
+	list($parentgrid, $parentlink) = explode("@", $gridstack[count($gridstack)-2]);
+
+	# Setup a redirect to a remote server if you choose a grid from pulldown menu. Tell
+	# destination server that we're walking foward in the grid tree.
+	if (strstr($clustername, "http://")) {
+		$tpl->assign("refresh", "0");
+		$tpl->assign("redirect", ";URL=$clustername?gw=fwd&gs=$gridstack_url");
+		echo "<h2>Redirecting, please wait...</h2>";
+		$tpl->printToScreen();
+		exit;
+	}
+
+	if( $view != "search" )
+		$tpl->assign( "refresh", $default_refresh );
+
+	$tpl->assign( "date", date("r") );
+	$tpl->assign( "page_title", $title );
+
+	# The page to go to when "Get Fresh Data" is pressed.
+	$tpl->assign("page","./");
+
+	# Templated Logo image
+	$tpl->assign("images","./templates/$template_name/images");
+
+	#
+	# Used when making graphs via graph.php. Included in most URLs
+	#
+	$sort_url=rawurlencode($sort);
+	$get_metric_string = "m=$metric&r=$range&s=$sort_url&hc=$hostcols";
+
+	if ($jobrange and $jobstart)
+		$get_metric_string .= "&jr=$jobrange&js=$jobstart";
+
+	# Set the Alternate view link.
+	$cluster_url=rawurlencode($clustername);
+	$node_url=rawurlencode($hostname);
+
+	# Make some information available to templates.
+	$tpl->assign("cluster_url", $cluster_url);
+	# Build the node_menu
+	$node_menu = "";
+
+	if ($parentgrid) {
+		$node_menu .= "<B>$parentgrid $meta_designator</B> ";
+		$node_menu .= "<B>&gt;</B>\n";
+	}
+
+	# Show grid.
+	$mygrid =  ($self == "unspecified") ? "" : $self;
+	$node_menu .= "<B><A HREF=\"../..\">$mygrid $meta_designator</A></B> ";
+	$node_menu .= "<B>&gt;</B>\n";
+
+	if ($physical)
+		$node_menu .= hiddenvar("p", $physical);
+
+	if ( $clustername ) {
+		$url = rawurlencode($clustername);
+		$node_menu .= "<B><A HREF=\"../../?c=".rawurlencode($clustername)."\">$clustername</A></B> ";
+		$node_menu .= "<B>&gt;</B>\n";
+		$node_menu .= hiddenvar("c", $clustername);
+	}
+
+	if (!count($metrics)) {
+		echo "<h4>Cannot find any metrics for selected cluster \"$clustername\", exiting.</h4>\n";       echo "Check ganglia XML tree (telnet $ganglia_ip $ganglia_port)\n";
+		exit;
+	}
+	$firsthost = key($metrics);
+	foreach ($metrics[$firsthost] as $m => $foo)
+		$context_metrics[] = $m;
+
+	foreach ($reports as $r => $foo)
+		$context_metrics[] = $r;
+
+	$node_menu .= "<B><A HREF=\"./?c=".rawurlencode($clustername)."\">Joblist</A></B> ";
+
+	if( isset( $hostname ) ) {
+
+		$node_menu .= "<B>&gt;</B>\n";
+		$href = "<A HREF=\"./?c=".rawurlencode($clustername)."&h=".$hostname."\">";
+		$node_menu .= "<B>$href";
+		$node_menu .= "host: $hostname</A></B> ";
+	}
+
+	if( count( $filter ) > 0 && $view != "search" ) {
+
+		$my_ct = 1;
+		$filter_nr = count( $filter );
+
+		foreach( $filter as $filtername=>$filterval ) {
+
+			$node_menu .= "<B>&gt;</B>\n";
+
+			$href = "<A HREF=\"./?c=".rawurlencode($clustername);
+			$temp_ct = 0;
+			$n_filter = $filter;
+			$my_filterorder = "";
+			$my_filters = array_keys( $filter );
+
+			foreach( $n_filter as $n_filtername=>$n_filterval ) {
+
+				if( $temp_ct < $my_ct ) {
+					$href .= "&". $n_filtername . "=" . $n_filterval;
+
+					if( $my_filterorder == "" )
+						$my_filterorder = $my_filters[$temp_ct];
+					else
+						$my_filterorder .= "," . $my_filters[$temp_ct];
+				}
+
+				$temp_ct++;
+			}
+			$href .= "&filterorder=$my_filterorder\">";
+
+			if( $my_ct < $filter_nr )
+				$node_menu .= "<B>$href$filtername: $filterval</A></B> ";
+			else
+				$node_menu .= "<B>$filtername: $filterval</B> ";
+
+			$my_ct++;
+		}
+	}
+
+	$m = $metricname;
+
+
+	$tpl->gotoBlock( "_ROOT" );
+	$tpl->assignGlobal("view", $view);
+
+	if( array_key_exists( "id", $filter ) or isset($hostname) ) {
+
+		//print_r( $context_metrics );
+
+		if( !isset( $hostname ) ) {
+
+			if (is_array($context_metrics) ) {
+				$metric_menu = "<B>Metric</B>&nbsp;&nbsp;"
+					."<SELECT NAME=\"m\" OnChange=\"".$form_name.".submit();\">\n";
+
+				sort($context_metrics);
+				foreach( $context_metrics as $k ) {
+					$url = rawurlencode($k);
+					$metric_menu .= "<OPTION VALUE=\"$url\" ";
+					if ($k == $metricname )
+						$metric_menu .= "SELECTED";
+					$metric_menu .= ">$k\n";
+				}
+				$metric_menu .= "</SELECT>\n";
+
+			}
+		}
+
+		$tpl->assign("metric_menu", $metric_menu );
+
+		if( $view == "search" or $view == "host" ) {
+			$tpl->newBlock("timeperiod");
+			if( is_numeric( $period_start ) ) {
+				$period_start = epochToDatetime( $period_start );
+			}
+			if( is_numeric( $period_stop ) ) {
+				$period_stop = epochToDatetime( $period_stop );
+			}
+			$tpl->assign("period_start", $period_start );
+			$tpl->assign("period_stop", $period_stop );
+			$tpl->assign("hostname", $hostname );
+
+			if( $view == "host" ) {
+				$tpl->newBlock("hostview");
+				$tpl->assign("job_start", $job_start );
+				$tpl->assign("job_stop", $job_stop );
+			}
+		} 
+
+	}
+
+	//$ex_fn = $tpl->getVarValue( "_ROOT", "form_name" );
+
+	if( $view == "search" or $view == "host" ) {
+
+		$node_menu .= "<B>&gt;</B>\n";
+		$node_menu .= "<B>Jobarchive</B> ";
+		$form_name = "archive_search_form";
+		$tpl->assignGlobal("form_name", $form_name );
+
+	} else {
+		$form_name = "toga_form";
+		$tpl->assignGlobal("form_name", $form_name );
+	}
+
+	if( $JOB_ARCHIVE && $page_call == 'index' ) {
+		$tpl->newBlock( "search" );
+		$tpl->assignGlobal( "cluster_url", rawurlencode($clustername) );
+		$tpl->assignGlobal( "cluster", $clustername );
+	}
+	$tpl->gotoBlock( "_ROOT" );
+	$tpl->assignGlobal( "cluster", $clustername );
+	$tpl->assign("node_menu", $node_menu);
+
+	# Make sure that no data is cached..
+	header ("Expires: Mon, 26 Jul 1997 05:00:00 GMT");    # Date in the past
+	header ("Last-Modified: " . gmdate("D, d M Y H:i:s") . " GMT"); # always modified
+	header ("Cache-Control: no-cache, must-revalidate");  # HTTP/1.1
+	header ("Pragma: no-cache");                          # HTTP/1.0
+}
+
+function makeFooter() {
+	global $tpl, $version, $parsetime, $monarchversion;
+
+	$tpl->gotoBlock( "_ROOT" );
+	$tpl->assign("webfrontend-version",$version["webfrontend"]);
+	$tpl->assign("monarch-version", $monarchversion);
+
+	if ($version["gmetad"]) {
+		$tpl->assign("webbackend-component", "gmetad");
+		$tpl->assign("webbackend-version",$version["gmetad"]);
+	} else if ($version["gmond"]) {
+		$tpl->assign("webbackend-component", "gmond");
+		$tpl->assign("webbackend-version", $version["gmond"]);
+	}
+
+	$tpl->assign("parsetime", sprintf("%.4f", $parsetime) . "s");
+}
+
+function includeSearchpage() {
+	global $tpl;
+
+	$tpl->assignInclude( "main", "templates/search.tpl" );
+
+}
+
+function includeOverview() {
+	global $tpl;
+
+	$tpl->assignInclude( "main", "templates/overview.tpl" );
+}
+
+function includeHostPage() {
+
+	global $tpl;
+
+	$tpl->assignInclude( "main", "templates/host_view.tpl" );
+}
+
+$tpl = new TemplatePower( "templates/index.tpl" );
+
+$tpl->assignInclude( "header", "templates/header.tpl" );
+
+if( isset( $h ) and $h != '' ) {
+	$hostname = $h;
+	//$view = "host";
+}
+
+switch( $view ) {
+
+	case "overview":
+
+		includeOverview();
+		break;
+
+	case "search":
+
+		includeSearchPage();
+		break;
+
+	//case "host":
+
+	//	includeHostPage();
+	//	break;
+
+	default:
+
+		includeOverview();
+		break;
+}
+
+$tpl->assignInclude( "footer", "templates/footer.tpl" );
+$tpl->prepare();
+
+$title = "Batch Report";
+makeHeader( 'index' );
+$tpl->assign("cluster_url", rawurlencode($clustername) );
+$tpl->assign("cluster", $clustername );
+
+switch( $view ) {
+
+	case "overview":
+
+		include "./overview.php";
+		makeOverview();
+		break;
+
+	case "search":
+
+		include "./search.php";
+		makeSearchPage();
+		break;
+
+	//case "host":
+
+	//	include "./host_view.php";
+	//	makeHostView();
+	//	break;
+
+	default:
+
+		makeOverview();
+		break;
+}
+
+makeFooter();
+$tpl->printToScreen();
+?>
Index: /tags/0.2/web/addons/job_monarch/libtoga.js
===================================================================
--- /tags/0.2/web/addons/job_monarch/libtoga.js	(revision 325)
+++ /tags/0.2/web/addons/job_monarch/libtoga.js	(revision 325)
@@ -0,0 +1,96 @@
+/*
+ *
+ * This file is part of Jobmonarch
+ *
+ * Copyright (C) 2006  Ramon Bastiaans
+ *
+ * Jobmonarch 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 2 of the License, or
+ * (at your option) any later version.
+ *
+ * Jobmonarch 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, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
+ *
+ * SVN $Id$
+ */
+
+function setSort( sortbyval ) {
+
+	if( sortbyval != document.toga_form.sortby.value ) {
+
+		document.toga_form.sortby.value = sortbyval;
+		document.toga_form.sortorder.value = "asc";
+
+	} else {
+
+		if( document.toga_form.sortorder.value == "desc" )
+			document.toga_form.sortorder.value = "asc";
+		else if( document.toga_form.sortorder.value == "asc" )
+			document.toga_form.sortorder.value = "desc";
+	}
+
+	document.forms['toga_form'].submit();
+}
+
+function setFilter( filtername, filterval ) {
+
+	//document.toga_form.id.value = '';
+	//document.toga_form.queue.value = '';
+	//document.toga_form.state.value = '';
+	//document.toga_form.user.value = '';
+	var myfilterorder = document.toga_form.elements['filterorder'].value;
+
+	if( document.toga_form.elements[filtername] ) {
+		document.toga_form.elements[filtername].value = filterval;
+		if( myfilterorder != '')
+			myfilterorder = myfilterorder + "," + filtername;
+		else
+			myfilterorder = filtername;
+
+	}
+	document.toga_form.elements['filterorder'].value = myfilterorder;
+
+	//setTimeout( "document.forms['toga_form'].submit();", 1000 );
+
+	document.forms['toga_form'].submit();
+}
+
+//function removeFilters( filters ) {
+
+	//var myfilter_fields = filters.split( " " );
+	//for( var i=0; i<myfilter_fields.length; i++ ) {
+		//removeFilter( myfilter_fields[i] );
+		//setTimeout( "removeFilter( "+myfilter_fields[i]+" );", 50 );
+	//}
+	// delay 100 ms before submit or fields might not be set
+	//document.forms['toga_form'].submit();", 100 );
+
+	//setTimeout( "document.forms['toga_form'].submit();", 1000 );
+//}
+
+//function removeFilter( filtername ) {
+
+//      var filterorder_fields = document.toga_form.elements['filterorder'].value.split( "," );
+//      var myfilterorder = '';
+
+//      for( var i=0; i<filterorder_fields.length; i++ ) {
+//              if( filterorder_fields[i] != filtername ) {
+//                      if( myfilterorder != '') {
+//                              myfilterorder = myfilterorder + "," + filterorder_fields[i];
+//                              //alert('myfilterorder = '+myfilterorder);
+//                      } else {
+//                              myfilterorder = filterorder_fields[i];
+//                              //alert('emyfilterorder = '+myfilterorder);
+//                      }
+//              }
+//      }
+//      document.toga_form.elements[filtername].value = '';
+//      document.toga_form.elements['filterorder'].value = myfilterorder;
+//}
Index: /tags/0.2/web/addons/job_monarch/libtoga.php
===================================================================
--- /tags/0.2/web/addons/job_monarch/libtoga.php	(revision 325)
+++ /tags/0.2/web/addons/job_monarch/libtoga.php	(revision 325)
@@ -0,0 +1,1351 @@
+<?php
+/*
+ *
+ * This file is part of Jobmonarch
+ *
+ * Copyright (C) 2006  Ramon Bastiaans
+ *
+ * Jobmonarch 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 2 of the License, or
+ * (at your option) any later version.
+ *
+ * Jobmonarch 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, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
+ *
+ * SVN $Id$
+ *
+ */
+
+
+class HTTPVariables {
+
+	var $clustername, $metricname;
+	var $restvars, $httpvars;
+
+	function HTTPVariables( $httpvars, $getvars ) {
+
+		$this->restvars = array();
+
+		$this->clustername = $httpvars["c"] ? $httpvars["c"] : $getvars["c"];
+		$this->metricname = $httpvars["m"] ? $httpvars["m"] : $getvars["m"];
+
+		foreach( $httpvars as $httpvar => $httpval ) {
+			
+			if( $httpval ) {
+				$this->restvars[$httpvar] = $httpval;
+			}
+		}
+
+		foreach( $getvars as $getvar => $getval ) {
+
+			if( $getval ) {
+				$this->restvars[$getvar] = $getval;
+			}
+		}
+	}
+
+	function getClusterName() {
+		return $this->clustername;
+	}
+
+	function getMetricName() {
+		return $this->metricname;
+	}
+
+	function getHttpVar( $var ) {
+		if( isset( $this->restvars[$var] ) )
+			return $this->restvars[$var];
+		else
+			return null;
+	}
+}
+
+// Toga's conf
+//
+include_once "./conf.php";
+include_once "./version.php";
+
+global $GANGLIA_PATH;
+global $RRDTOOL;
+global $JOB_ARCHIVE_DIR;
+global $JOB_ARCHIVE_DBASE;
+
+$my_dir = getcwd();
+
+// Load Ganglia's PHP
+chdir( $GANGLIA_PATH );
+
+include_once "./conf.php";
+include_once "./functions.php";
+include_once "./ganglia.php";
+include_once "./get_context.php";
+unset( $start );
+$context = 'cluster';
+include_once "./get_ganglia.php";
+
+// Back to our PHP
+chdir( $my_dir );
+
+global $SMALL_CLUSTERIMAGE_MAXWIDTH, $SMALL_CLUSTERIMAGE_NODEWIDTH, $DATA_SOURCE, $HTTP_GET_VARS, $_GET;
+$httpvars = new HTTPVariables( $HTTP_GET_VARS, $_GET );
+
+// Set cluster context so that Ganglia will
+// provide us with the correct metrics array
+//
+global $context, $clustername, $reports;
+
+//$clustername = $httpvars->getClusterName();
+
+global $default_metric;
+
+// Ganglia's array of host metrics
+//
+global $metrics, $hosts_up;
+
+global $DATETIME_FORMAT;
+
+function makeDate( $time ) {
+	global $DATETIME_FORMAT;
+	return strftime( $DATETIME_FORMAT, $time );
+}
+
+class TarchDbase {
+
+	var $ip, $dbase, $conn;
+
+	function TarchDbase( $ip = null, $dbase = null ) {
+
+		global $JOB_ARCHIVE_DBASE;
+
+		$db_fields = explode( '/', $JOB_ARCHIVE_DBASE );
+
+		$this->ip = $db_fields[0];
+		$this->dbase = $db_fields[1];
+		$this->conn = null;
+	}
+
+	function connect() {
+
+		if( $this->ip == null )
+			$this->conn = pg_connect( "dbname=".$this->dbase );
+		else
+			$this->conn = pg_connect( "host=".$this->ip." dbname=".$this->dbase );
+	}
+
+	function searchDbase( $id = null, $queue = null, $user = null, $name = null, $start_from_time = null, $start_to_time = null, $end_from_time = null, $end_to_time = null ) {
+
+		global $SEARCH_RESULT_LIMIT;
+
+		if( $id ) {
+			$select_query = "SELECT job_id FROM jobs WHERE job_id = '$id' AND job_status = 'F'";
+			$this->resultcount = 1;
+		} else {
+			$query_args = array();
+			
+			if( $queue )
+				$query_args[] = "job_queue ='$queue'";
+			if( $user )
+				$query_args[] = "job_owner ='$user'";
+			if( $name )
+				$query_args[] = "job_name = '$name'";
+			if( $start_from_time )
+				$query_args[] = "job_start_timestamp >= $start_from_time";
+			if( $start_to_time )
+				$query_args[] = "job_start_timestamp <= $start_to_time";
+			if( $end_from_time )
+				$query_args[] = "job_stop_timestamp >= $end_from_time";
+			if( $end_to_time )
+				$query_args[] = "job_stop_timestamp <= $end_to_time";
+
+			$query = "FROM jobs WHERE job_status = 'F' AND ";
+			$extra_query_args = '';
+
+			foreach( $query_args as $myquery ) {
+
+				if( $extra_query_args == '' )
+					$extra_query_args = $myquery;
+				else
+					$extra_query_args .= " AND ".$myquery;
+			}
+			$query .= $extra_query_args;
+
+			$count_result_idname = "COUNT(job_id)";
+			$select_result_idname = "job_id";
+
+			$count_query = "SELECT " . $count_result_idname . " " . $query;
+
+			$count_result = $this->queryDbase( $count_query );
+			$this->resultcount = (int) $count_result[0][count];
+
+			$select_query = "SELECT " . $select_result_idname . " " . $query . " ORDER BY job_id LIMIT " . $SEARCH_RESULT_LIMIT;
+		}
+
+		$ids = $this->queryDbase( $select_query );
+
+		$ret = array();
+
+		foreach( $ids as $crow)
+			$ret[] = $crow[job_id];
+
+		return $ret;
+	}
+
+	function getNodesForJob( $jobid ) {
+
+		$result = $this->queryDbase( "SELECT node_id FROM job_nodes WHERE job_id = '$jobid'" );
+
+		$nodes = array();
+
+		foreach( $result as $result_row ) 
+
+			$nodes[] = $this->getNodeArray( $result_row[node_id] );
+
+		return $nodes;
+	}
+
+	function getJobsForNode( $nodeid ) {
+
+		$result = $this->queryDbase( "SELECT job_id FROM job_nodes WHERE node_id = '$nodeid'" );
+
+		$jobs = array();
+
+		foreach( $result as $result_row )
+
+			$jobs[] = $this->getJobArray( $result_row[job_id] );
+
+		return $jobs;
+	}
+
+	function getJobArray( $id ) {
+		$result = $this->queryDbase( "SELECT * FROM jobs WHERE job_id = '$id'" );
+
+		return ( $this->makeArray( $result[0] ) );
+	}
+
+	function getNodeArray( $id ) {
+
+		$result = $this->queryDbase( "SELECT * FROM nodes WHERE node_id = '$id'" );
+
+		return ( $this->makeArray( $result[0] ) );
+	}
+
+	function makeArray( $result_row ) {
+
+		$myar = array();
+
+		foreach( $result_row as $mykey => $myval ) {
+
+			$map_key = explode( '_', $mykey );
+
+			$rmap_key = array_reverse( $map_key );
+			array_pop( $rmap_key );
+			$map_key = array_reverse( $rmap_key );
+			
+			$newkey = implode( '_', $map_key );
+			
+			$myar[$newkey] = $result_row[$mykey];
+		}
+
+		return $myar;
+	}
+
+	function queryDbase( $query ) {
+
+		$result_rows = array();
+	
+		if( !$this->conn )
+			$this->connect();
+
+		//printf( "query = [%s]\n", $query );
+		$result = pg_query( $this->conn, $query );
+
+		while ($row = pg_fetch_assoc($result))
+			$result_rows[] = $row;
+
+		return $result_rows;
+	}
+}
+
+class TarchRrdGraph {
+	var $rrdbin, $rrdvalues, $clustername, $hostname, $tempdir, $tarchdir, $metrics;
+
+	function TarchRrdGraph( $clustername, $hostname ) {
+
+		global $RRDTOOL;
+		global $JOB_ARCHIVE_DIR;
+
+		$this->rrdbin = $RRDTOOL;
+		$this->rrdvalues = array();
+		$this->tarchdir = $JOB_ARCHIVE_DIR;
+		$this->clustername = $clustername;
+		$this->hostname = $hostname;
+	}
+
+	function doCmd( $command ) {
+
+		printf( "command = %s\n", $command );
+		$pipe = popen( $command . ' 2>&1', 'r' );
+
+		if (!$pipe) {
+			print "pipe failed.";
+			return "";
+		}
+
+		$output = '';
+		while(!feof($pipe))
+			$output .= fread($pipe, 1024);
+
+		pclose($pipe);
+
+		$output = explode( "\n", $output );
+		//print_r( $output );
+		return $output;
+	}
+
+	function dirList( $dir ) {
+
+		$dirlist = array();
+
+		if ($handle = opendir( $dir )) {
+			while (false !== ($file = readdir($handle))) {
+				if ($file != "." && $file != "..") {
+					$dirlist[] = $file;
+				}
+			}
+			closedir($handle);
+		}
+
+		return $dirlist;
+	}
+
+	function getTimePeriods( $start, $end ) {
+
+		//printf("start = %s end = %s\n", $start, $end );
+		$times = array();
+		$dirlist = $this->dirList( $this->tarchdir . '/' . $this->clustername . '/' . $this->hostname );
+
+		//print_r( $dirlist );
+
+		$first = 0;
+		$last = 9999999999999;
+
+		foreach( $dirlist as $dir ) {
+
+			if( $dir > $first and $dir <= $start )
+				$first = $dir;
+			if( $dir < $last and $dir >= $end )
+				$last = $dir;
+		}
+
+		//printf( "first = %s last = %s\n", $first, $last );
+
+		foreach( $dirlist as $dir ) {
+
+			//printf( "dir %s ", $dir );
+
+			if( $dir >= $first and $dir <= $last and !array_key_exists( $dir, $times ) ) {
+			
+				$times[] = $dir;
+				//printf("newtime %s ", $dir );
+
+			}
+		}
+
+		//print_r( $times );
+
+		sort( $times );
+
+		//print_r( $times );
+
+		return $times;
+	}
+
+	function getRrdDirs( $start, $stop ) {
+
+		//printf( "tarchdir = %s\n", $this->tarchdir );
+		$timess = $this->getTimePeriods( $start, $stop );
+		//print_r( $timess );
+
+		$rrd_files = array();
+
+		foreach( $timess as $time ) {
+
+			$rrd_files[] = $this->tarchdir . '/' . $this->clustername . '/' . $this->hostname. '/'.$time;
+		}
+
+		return $rrd_files;
+	}
+
+	function getRrdFiles( $metric, $start, $stop ) {
+
+		$times = $this->getTimePeriods( $start, $stop );
+
+		$rrd_files = array();
+
+		foreach( $times as $time ) {
+
+			$rrd_files[] = $this->tarchdir . '/' . $this->clustername . '/' . $this->hostname . '/' .$time. '/' . $metric. '.rrd';
+		}
+
+		return $rrd_files;
+	}
+
+	function graph( $descr ) {
+//	monitor2:/data/toga/rrds/LISA Cluster/gb-r15n11.irc.sara.nl# rrdtool graph /var/www/ganglia/test1.png --start 1118683231 --end 1118750431 --width 300 --height 400 DEF:'1'='./1118647515/load_one.rrd':'sum':AVERAGE DEF:'2'='./1118690723/load_one.rrd':'sum':AVERAGE DEF:'3'='./1118733925/load_one.rrd':'sum':AVERAGE AREA:1#555555:"load_one" AREA:2#555555 AREA:3#555555
+//	380x461
+//	monitor2:/data/toga/rrds/LISA Cluster/gb-r15n11.irc.sara.nl#
+		//$command = $this->rrdbin . " graph - --start $start --end $end ".
+			"--width $width --height $height $upper_limit $lower_limit ".
+			"--title '$title' $vertical_label $extras $background ". $series;
+
+		//$graph = $this->doCmd( $command );
+
+		//return $graph;
+		return 0;
+	}
+}
+
+class DataSource {
+
+	var $data, $ip, $port;
+
+	function DataSource( $ip = '127.0.0.1', $port = 8649 ) {
+		$this->ip = $ip;
+		$this->port = $port;
+	}
+
+	function getData() {
+
+		$errstr;
+		$errno = 0;
+		$timeout = 3;
+
+		$fp = fsockopen( $this->ip, $this->port, $errno, $errstr, $timeout );
+
+		if( !$fp ) {
+			echo 'Unable to connect to '.$this->ip.':'.$this->port; // printf( 'Unable to connect to [%s:%.0f]', $this->ip, $this->port );
+			return;
+		}
+
+		stream_set_timeout( $fp, 30 );
+
+		while ( !feof( $fp ) ) {
+			
+			$data .= fread( $fp, 16384 );
+		}
+
+		fclose( $fp );
+
+		return $data;
+	}
+}
+
+class DataGatherer {
+
+	var $xmlhandler, $data, $httpvars;
+
+	function DataGatherer( $cluster ) {
+
+		global $DATA_SOURCE;
+	
+		//printf("dg cluster = %s\n", $cluster );
+		$ds_fields = explode( ':', $DATA_SOURCE );
+		$ds_ip = $ds_fields[0];
+		$ds_port = $ds_fields[1];
+
+		$this->source = new DataSource( $ds_ip, $ds_port );
+
+		$this->parser = xml_parser_create();
+		$this->httpvars = $httpvars;
+		$this->xmlhandler = new TorqueXMLHandler( $cluster );
+		xml_set_element_handler( $this->parser, array( &$this->xmlhandler, 'startElement' ), array( &$this->xmlhandler, 'stopElement' ) );
+	}
+
+	function parseXML() {
+
+		$src = &$this->source;
+		$this->data = $src->getData();
+
+		if ( !xml_parse( $this->parser, $this->data ) )
+			$error = sprintf( 'XML error: %s at %d', xml_error_string( xml_get_error_code( $this->parser ) ), xml_get_current_line_number( $this->parser ) );
+	}
+
+	function printInfo() {
+		$handler = $this->xmlhandler;
+		$handler->printInfo();
+	}
+
+	function getNodes() {
+		$handler = $this->xmlhandler;
+		return $handler->getNodes();
+	}
+
+	function getNode( $node ) {
+		$handler = $this->xmlhandler;
+		return $handler->getNode( $node );
+	}
+
+	function getCpus() {
+		$handler = $this->xmlhandler;
+		return $handler->getCpus();
+	}
+
+	function getJobs() {
+		$handler = $this->xmlhandler;
+		return $handler->getJobs();
+	}
+
+	function getJob( $job ) {
+		$handler = $this->xmlhandler;
+		return $handler->getJob( $job );
+	}
+
+	function getHeartbeat() {
+		$handler = $this->xmlhandler;
+		return $handler->getHeartbeat();
+	}
+
+	function isJobmonRunning() {
+		$handler = $this->xmlhandler;
+		return $handler->isJobmonRunning();
+	}
+}
+
+class TorqueXMLHandler {
+
+	var $clusters, $heartbeat, $nodes, $jobs, $clustername, $proc_cluster;
+
+	function TorqueXMLHandler( $clustername ) {
+		$jobs = array();
+		$clusters = array();
+		$this->nodes = array();
+		$heartbeat = array();
+		$this->clustername = $clustername;
+		//printf(" cluster set to %s \n", $this->clustername );
+	}
+
+	function getCpus() {
+
+		$cpus = 0;
+
+		if( isset( $this->jobs ) && count( $this->jobs ) > 0 ) {
+
+			foreach( $this->jobs as $jobid=>$jobattrs ) {
+
+				$nodes = count( $jobattrs[nodes] );
+				$ppn = (int) $jobattrs[ppn] ? $jobattrs[ppn] : 1;
+				$mycpus = $nodes * $ppn;
+
+				$cpus = $cpus + $mycpus;
+			}
+		}
+	}
+
+	function isJobmonRunning() {
+
+		if (isset( $this->heartbeat['time'] ))
+			return 1;
+		else
+			return 0;
+	}
+
+	function startElement( $parser, $name, $attrs ) {
+
+		$jobs = $this->jobs;
+		$nodes = $this->nodes;
+
+		if ( $attrs[TN] ) {
+
+			// Ignore dead metrics. Detect and mask failures.
+			if ( $attrs[TN] > $attrs[TMAX] * 4 )
+				return;
+		}
+
+		$jobid = null;
+
+		//printf( '%s=%s', $attrs[NAME], $attrs[VAL] );
+
+		//printf( "clustername = %s proc_cluster = %s\n", $this->clustername, $this->proc_cluster );
+
+		if( $name == 'CLUSTER' ) {
+
+			$this->proc_cluster = $attrs[NAME];
+			//printf( "Found cluster %s\n", $attrs[NAME] );
+			//print_r( $attrs );
+
+			//if( !isset( $clusters[$clustername] ) )
+			//	$clusters[$clustername] = array();
+
+		} else if( $name == 'HOST' and $this->proc_cluster == $this->clustername) {
+
+			$hostname = $attrs[NAME];
+			$location = $attrs[LOCATION];
+			//printf( "Found node %s\n", $hostname );
+
+			if( !isset( $nodes[$hostname] ) )
+				$nodes[$hostname] = new NodeImage( $hostname );
+
+		} else if( $name == 'METRIC' and strstr( $attrs[NAME], 'MONARCH' ) and $this->proc_cluster == $this->clustername ) {
+
+			if( strstr( $attrs[NAME], 'MONARCH-HEARTBEAT' ) ) {
+
+				$this->heartbeat['time'] = $attrs[VAL];
+				//printf( "heartbeat %s\n", $heartbeat['time'] );
+
+			} else if( strstr( $attrs[NAME], 'MONARCH-JOB' ) ) {
+
+				sscanf( $attrs[NAME], 'MONARCH-JOB-%d-%d', $jobid, $monincr );
+
+				//printf( "jobid %s\n", $jobid );
+
+				if( !isset( $jobs[$jobid] ) )
+					$jobs[$jobid] = array();
+
+				$fields = explode( ' ', $attrs[VAL] );
+
+				foreach( $fields as $f ) {
+					$togavalues = explode( '=', $f );
+
+					$toganame = $togavalues[0];
+					$togavalue = $togavalues[1];
+
+					//printf( "\t%s\t= %s\n", $toganame, $togavalue );
+
+					if( $toganame == 'nodes' ) {
+
+						if( $jobs[$jobid][status] == 'R' ) {
+						
+							if( !isset( $jobs[$jobid][$toganame] ) )
+								$jobs[$jobid][$toganame] = array();
+
+							$mynodes = explode( ';', $togavalue );
+
+							//print_r($mynodes);
+
+							foreach( $mynodes as $node ) {
+
+								if( !in_array( $node, $jobs[$jobid][$toganame] ) ) {
+									$jobs[$jobid][$toganame][] = $node;
+								}
+							}
+
+						} else if( $jobs[$jobid][status] == 'Q' ) {
+
+							$jobs[$jobid][$toganame] = $togavalue;
+						}
+						
+					} else {
+
+						$jobs[$jobid][$toganame] = $togavalue;
+					}
+				}
+
+				if( isset( $jobs[$jobid][domain] ) and isset( $jobs[$jobid][nodes] ) ) {
+			
+					$nr_nodes = count( $jobs[$jobid][nodes] );
+		
+					if( $jobs[$jobid][status] == 'R' ) {
+
+						foreach( $jobs[$jobid][nodes] as $node ) {
+
+							$domain = $jobs[$jobid][domain];
+							$domain_len = 0 - strlen( $domain );
+
+							if( substr( $node, $domain_len ) != $domain ) {
+								$host = $node. '.'.$domain;
+							} else {
+								$host = $node;
+							}
+
+							//$host = $node.'.'.$jobs[$jobid][domain];
+				
+							if( !isset( $nodes[$host] ) )
+								$my_node = new NodeImage( $host );
+							else
+								$my_node = $nodes[$host];
+
+							if( !$my_node->hasJob( $jobid ) )
+
+								if( isset( $jobs[$jobid][ppn] ) )
+									$my_node->addJob( $jobid, ((int) $jobs[$jobid][ppn]) );
+								else
+									$my_node->addJob( $jobid, 1 );
+
+							$nodes[$host] = $my_node;
+						}
+					}
+				}
+			}
+		}
+		$this->jobs = $jobs;
+		//print_r( $nodes );
+		$this->nodes = $nodes;
+		//print_r( $this->nodes );
+	}
+
+	function stopElement( $parser, $name ) {
+	}
+
+	function printInfo() {
+
+		$jobs = &$this->jobs;
+
+		printf( "---jobs---\n" );
+
+		foreach( $jobs as $jobid => $job ) {
+
+			printf( "job %s\n", $jobid );
+
+			if( isset( $job[nodes] ) ) {
+
+				foreach( $job[nodes] as $node ) {
+
+					$mynode = $this->nodes[$node];
+					$hostname = $mynode->getHostname();
+					$location = $mynode->getLocation();
+
+					printf( "\t- node %s\tlocation %s\n", $hostname, $location );
+					//$this->nodes[$hostname]->setLocation( "hier draait job ".$jobid );
+				}
+			}
+		}
+
+		printf( "---nodes---\n" );
+
+		$nodes = &$this->nodes;
+
+		foreach( $nodes as $node ) {
+
+			$hostname = $node->getHostname();
+			$location = $node->getLocation();
+			$jobs = implode( ' ', $node->getJobs() );
+			printf( "* node %s\tlocation %s\tjobs %s\n", $hostname, $location, $jobs );
+		}
+	}
+
+	function getNodes() {
+		//print_r( $this->nodes );
+		return $this->nodes;
+	}
+
+	function getNode( $node ) {
+
+		$nodes = &$this->nodes;
+		if( isset( $nodes[$node] ) )
+			return $nodes[$node];
+		else
+			return NULL;
+	}
+
+	function getJobs() {
+		return $this->jobs;
+	}
+
+	function getJob( $job ) {
+
+		$jobs = &$this->jobs;
+		if( isset( $jobs[$job] ) )
+			return $jobs[$job];
+		else
+			return NULL;
+	}
+
+	function getHeartbeat() {
+		return $this->heartbeat['time'];
+	}
+}
+
+class NodeImage {
+
+	var $image, $x, $y, $hostname, $jobs, $tasks, $showinfo;
+
+	function NodeImage( $hostname ) {
+
+		global $SMALL_CLUSTERIMAGE_NODEWIDTH;
+
+		$this->jobs = array();
+		//$this->image = $image;
+		//$this->x = $x;
+		//$this->y = $y;
+		$this->tasks = 0;
+		$this->hostname = $hostname;
+		$this->cpus = $this->determineCpus();
+		$this->showinfo = 1;
+		$this->size = $SMALL_CLUSTERIMAGE_NODEWIDTH;
+	}
+
+	function addJob( $jobid, $cpus ) {
+		$jobs = &$this->jobs;
+
+		$jobs[] = $jobid;
+		$this->jobs = $jobs;
+
+		$this->addTask( $cpus );
+	}
+
+	function hasJob( $jobid ) {
+
+		$jobfound = 0;
+
+		if( count( $this->jobs ) > 0 )
+			foreach( $this->jobs as $job )
+
+				if( $job == $jobid )
+					$jobfound = 1;
+
+		return $jobfound;
+	}
+
+	function addTask( $cpus ) {
+
+		$this->tasks = $this->tasks + $cpus;
+	}
+
+	function setImage( $image ) {
+
+		$this->image = $image;
+	}
+
+	function setCoords( $x, $y ) {
+
+		$this->x = $x;
+		$this->y = $y;
+	}
+
+	function colorHex( $color ) {
+	
+		$my_color = imageColorAllocate( $this->image, hexdec( substr( $color, 0, 2 )), hexdec( substr( $color, 2, 2 )), hexdec( substr( $color, 4, 2 )) );
+
+		return $my_color;
+	}
+
+	function setLoad( $load ) {
+		$this->load = $load;
+	}
+
+	function setHostname( $hostname ) {
+		$this->hostname = $hostname;
+	}
+
+	function getHostname() {
+		return $this->hostname;
+	}
+
+	function getJobs() {
+		return $this->jobs;
+	}
+
+	function setShowinfo( $showinfo ) {
+		$this->showinfo = $showinfo;
+	}
+
+	function drawSmall() {
+
+		global $SMALL_CLUSTERIMAGE_NODEWIDTH;
+
+		$this->size	= $SMALL_CLUSTERIMAGE_NODEWIDTH;
+
+		$this->draw();
+	}
+
+	function drawBig() {
+
+		global $BIG_CLUSTERIMAGE_NODEWIDTH;
+
+		$this->size	= $BIG_CLUSTERIMAGE_NODEWIDTH;
+
+		$this->draw();
+	}
+
+	function draw() {
+
+		global $JOB_NODE_MARKING;
+
+		$black_color = imageColorAllocate( $this->image, 0, 0, 0 );
+		$size = $this->size;
+
+		imageFilledRectangle( $this->image, $this->x, $this->y, $this->x+($size), $this->y+($size), $black_color );
+
+		if( $this->showinfo) {
+		
+			$this->load = $this->determineLoad();
+
+			if( !isset( $this->image ) or !isset( $this->x ) or !isset( $this->y ) ) {
+				printf( "aborting\n" );
+				printf( "x %d y %d load %f\n", $this->x, $this->y, $load );
+				return;
+			}
+
+
+			// Convert Ganglias Hexadecimal load color to a Decimal one
+			//
+			$load = $this->determineLoad();	
+			$usecolor = $this->colorHex( load_color($load) );
+			imageFilledRectangle( $this->image, $this->x+1, $this->y+1, $this->x+($size-1), $this->y+($size-1), $usecolor );
+			if( count( $this->jobs ) > 0 )
+				imageString( $this->image, 1, $this->x+(($size/2)-1), $this->y+(($size/2)-4), $JOB_NODE_MARKING, $black_color );
+
+		} else {
+
+			// White
+			$usecolor = imageColorAllocate( $this->image, 255, 255, 255 );
+			imageFilledRectangle( $this->image, $this->x+1, $this->y+1, $this->x+($size-1), $this->y+($size-1), $usecolor );
+		}
+
+
+	}
+
+	function determineCpus() {
+
+		global $metrics;
+
+		$cpus = $metrics[$this->hostname][cpu_num][VAL];
+		if (!$cpus) $cpus=1;
+
+		return $cpus;
+	}
+
+	function determineLoad() {
+
+		global $metrics;
+
+		$load_one = $metrics[$this->hostname][load_one][VAL];
+		$load = ((float) $load_one)/$this->cpus;
+
+		return $load;
+	}
+}
+
+class ClusterImage {
+
+	var $dataget, $image, $clustername;
+	var $filtername, $filters;
+
+	function ClusterImage( $clustername ) {
+
+		$this->dataget		= new DataGatherer( $clustername );
+		$this->clustername	= $clustername;
+		$this->filters		= array();
+		$this->size		= 's';
+	}
+
+	function setSmall() {
+		$this->size	= 's';
+	}
+
+	function setBig() {
+		$this->size	= 'b';
+	}
+
+	function isSmall() {
+		return ($this->size == 's');
+	}
+
+	function isBig() {
+		return ($this->size == 'b');
+	}
+
+	function setFilter( $filtername, $filtervalue ) {
+
+		$this->filters[$filtername] = $filtervalue;
+	}
+
+	function filterNodes( $jobs, $nodes ) {
+
+		$filtered_nodes = array();
+
+		foreach( $nodes as $node ) {
+
+			$hostname = $node->getHostname();
+
+			$addhost = 1;
+
+			if( count( $this->filters ) > 0 ) {
+
+				$mynjobs = $node->getJobs();
+
+				if( count( $mynjobs ) > 0 ) {
+
+					foreach( $mynjobs as $myjob ) {
+
+						foreach( $this->filters as $filtername => $filtervalue ) {
+
+							if( $filtername!=null && $filtername!='' ) {
+
+								if( $filtername == 'jobid' && !$node->hasJob( $filtervalue) ) {
+									$addhost = 0;
+								} else if( $filtername != 'jobid' ) {
+									if( $jobs[$myjob][$filtername] != $filtervalue ) {
+										$addhost = 0;
+									}
+								}
+							}
+						}
+					}
+				} else
+					$addhost = 0;
+			}
+
+			if( $addhost )
+				$filtered_nodes[] = $hostname;
+		}
+
+		return $filtered_nodes;
+	}
+
+	function draw() {
+
+		global $SMALL_CLUSTERIMAGE_MAXWIDTH, $SMALL_CLUSTERIMAGE_NODEWIDTH;
+		global $BIG_CLUSTERIMAGE_MAXWIDTH, $BIG_CLUSTERIMAGE_NODEWIDTH;
+	
+		$mydatag = $this->dataget;
+		$mydatag->parseXML();
+
+		if( $this->isSmall() ) {
+			$max_width = $SMALL_CLUSTERIMAGE_MAXWIDTH;
+			$node_width = $SMALL_CLUSTERIMAGE_NODEWIDTH;
+		} else if( $this->isBig() ) {
+			$max_width = $BIG_CLUSTERIMAGE_MAXWIDTH;
+			$node_width = $BIG_CLUSTERIMAGE_NODEWIDTH;
+		}
+
+		$nodes = $mydatag->getNodes();
+		$nodes_hosts = array_keys( $nodes );
+
+		$nodes_nr = count( $nodes );
+
+		$nodes_size = $nodes_nr*$node_width;
+		$node_rows = 0;
+
+		if( $nodes_size > $max_width ) {
+			$nodes_per_row = ( (int) ($max_width/$node_width) );
+		} else {
+			$nodes_per_row = $nodes_size;
+			$node_rows = 1;
+		}
+
+		if( $nodes_per_row < $nodes_nr ) {
+			$node_rows = ( (int) ($nodes_nr/$nodes_per_row) );
+			$node_rest = fmod( $nodes_nr, $nodes_per_row );
+			//printf( "nodesnr %d noderest %f\n", $nodes_nr, $node_rest );
+			if( $node_rest > 0 ) {
+				$node_rows++;
+				//printf( "noderows %d\n", $node_rows );
+			}
+		}
+
+		$y_offset	= 0;
+		$font 		= 2;
+		$fontheight	= ImageFontHeight( $font );
+		$fontspaceing	= 2;
+		$y_offset	= $fontheight + (2 * $fontspaceing);
+
+		$image = imageCreateTrueColor( $max_width, ($y_offset + (($node_rows*$node_width)+1) ) );
+		$colorwhite = imageColorAllocate( $image, 255, 255, 255 );
+		imageFill( $image, 0, 0, $colorwhite );
+
+		if( $this->isSmall() ) {
+
+			$colorblue	= imageColorAllocate( $image, 0, 0, 255 );
+
+			imageString( $image, $font, 2, 2, "Monarch Joblist - cluster: ".$this->clustername, $colorblue );
+		}
+
+		$jobs = $mydatag->getJobs();
+		//printf("filtername = %s\n", $filtername );
+		$filtered_nodes = $this->filterNodes( $jobs, $nodes );
+
+		//print_r($filtered_nodes);
+
+		for( $n = 0; $n < $node_rows; $n++ ) {
+			
+			for( $m = 0; $m < $nodes_per_row; $m++ ) {
+			
+				$x = ($m * $node_width);
+				$y = $y_offset + ($n * $node_width);
+
+				$cur_node = ($n * $nodes_per_row) + ($m);
+				$host = $nodes_hosts[$cur_node];
+
+				if( isset( $nodes[$host] ) ) {
+
+					$nodes[$host]->setCoords( $x, $y );
+					$nodes[$host]->setImage( $image );
+
+					if( !in_array( $host, $filtered_nodes ) )
+						$nodes[$host]->setShowinfo( 0 );
+
+					if( $this->isSmall() )
+						$nodes[$host]->drawSmall();
+					else if( $this->isBig() )
+						$nodes[$host]->drawBig();
+				}
+			}
+		}
+		
+		header( 'Content-type: image/png' );
+		imagePNG( $image );
+		imageDestroy( $image );
+	}
+}
+
+class EmptyImage {
+
+	function draw() {
+		$image		= imageCreateTrueColor( 1, 1 );
+		$colorwhite	= imageColorAllocate( $image, 255, 255, 255 );
+		imageFill( $image, 0, 0, $colorwhite );                         
+
+		header( 'Content-type: image/png' );
+		imagePNG( $image );
+		imageDestroy( $image );
+	}
+}
+
+class HostImage {
+
+	var $data_gather, $cluster, $host, $node, $image;
+	var $headerstrlen;
+
+	function HostImage( $data_gather, $cluster, $host ) {
+
+		$this->data_gather 	= $data_gather;
+		$this->cluster		= $cluster;
+		$this->host		= $host;
+		$this->y_offset		= 0;
+		$this->font		= 2;
+		$this->fontspaceing	= 2;
+		$this->headerstrlen	= array();
+
+		$this->fontheight	= ImageFontHeight( $this->font );
+		$this->fontwidth	= ImageFontWidth( $this->font );
+
+		$dg			= &$this->data_gather;
+		$this->node		= &$dg->getNode( $this->host );
+		$n			= &$this->node;
+		$this->njobs		= $n->getJobs();
+	}
+
+	function drawJobs() {
+
+		$dg                     = &$this->data_gather;
+		$colorblack		= imageColorAllocate( $this->image, 0, 0, 0 );
+
+		for( $n = 0; $n < count( $this->njobs ); $n++ ) {
+
+			$jobid			= $this->njobs[$n];
+			$jobinfo		= $dg->getJob( $jobid );
+
+			$xoffset		= 5;
+			imageString( $this->image, $this->font, $xoffset, $this->y_offset, strval( $jobid ), $colorblack );
+
+			foreach( $this->headerstrlen as $headername => $headerlen ) {
+
+				if( $headername == 'nodes' ) {
+					$attrval	= strval( count( $jobinfo[nodes] ) );
+				} else if( $headername == 'cpus' ) {
+
+					if( !isset( $jobinfo[ppn] ) )
+						$jobinfo[ppn] = 1;
+
+					$attrval	= strval( count( $jobinfo[nodes] ) * intval( $jobinfo[ppn] ) );
+
+				} else if( $headername == 'runningtime' ) {
+					$attrval	= makeTime( intval( $jobinfo[reported] ) - intval( $jobinfo[start_timestamp] ) );
+				} else {
+					$attrval	= strval( $jobinfo[$headername] );
+				}
+
+				imageString( $this->image, $this->font, $xoffset, $this->y_offset, $attrval, $colorblack );
+		
+				$xoffset	= $xoffset + ($this->fontwidth * ( $headerlen + 1 ) );
+
+			}
+			
+			$this->newLineOffset();
+		}
+	}
+
+	function drawHeader() {
+
+		$dg                     = &$this->data_gather;
+
+		for( $n = 0; $n < count( $this->njobs ); $n++ ) {
+
+			$jobid			= $this->njobs[$n];
+			$jobinfo		= $dg->getJob( $jobid );
+
+			if( !isset( $this->headerstrlen[id] ) )
+				$this->headerstrlen[id]	= strlen( strval( $jobid ) );
+			else
+				if( strlen( strval( $jobid ) ) > $this->headerstrlen[id] )
+					$this->headerstrlen[id]	= strlen( strval( $jobid ) );
+
+			if( !isset( $this->headerstrlen[owner] ) )
+				$this->headerstrlen[owner]	= strlen( strval( $jobinfo[owner] ) );
+			else
+				if( strlen( strval( $jobinfo[owner] ) ) > $this->headerstrlen[owner] )
+					$this->headerstrlen[owner]	= strlen( strval( $jobinfo[owner] ) );
+
+			if( !isset( $this->headerstrlen[queue] ) )
+				$this->headerstrlen[queue]	= strlen( strval( $jobinfo[queue] ) );
+			else
+				if( strlen( strval( $jobinfo[queue] ) ) > $this->headerstrlen[queue] )
+					$this->headerstrlen[queue]	= strlen( strval( $jobinfo[queue] ) );
+
+			if( !isset( $jobinfo[ppn] ) )
+				$jobinfo[ppn] = 1;
+
+			$cpus			= count( $jobinfo[nodes] ) * intval( $jobinfo[ppn] );
+
+			if( !isset( $this->headerstrlen[cpus] ) )
+				$this->headerstrlen[cpus]	= strlen( strval( $cpus ) );
+			else
+				if( strlen( strval( $cpus ) ) > $this->headerstrlen[cpus] )
+					$this->headerstrlen[cpus]	= strlen( strval( $cpus ) );
+
+			$nodes			= count( $jobinfo[nodes] );
+
+			if( !isset( $this->headerstrlen[nodes] ) )
+				$this->headerstrlen[nodes]	= strlen( strval( $nodes ) );
+			else
+				if( strlen( strval( $nodes) ) > $this->headerstrlen[nodes] )
+					$this->headerstrlen[nodes]	= strlen( strval( $nodes ) );
+
+			$runningtime		= makeTime( intval( $jobinfo[reported] ) - intval( $jobinfo[start_timestamp] ) );
+
+			if( !isset( $this->headerstrlen[runningtime] ) )
+				$this->headerstrlen[runningtime]	= strlen( strval( $runningtime) );
+			else
+				if( strlen( strval( $runningtime) ) > $this->headerstrlen[runningtime] )
+					$this->headerstrlen[runningtime]	= strlen( strval( $runningtime) );
+
+			if( !isset( $this->headerstrlen[name] ) )
+				$this->headerstrlen[name]	= strlen( strval( $jobinfo[name] ) );
+			else
+				if( strlen( strval( $jobinfo[name] ) ) > $this->headerstrlen[name] )
+					$this->headerstrlen[name]	= strlen( strval( $jobinfo[name] ) );
+
+		}
+
+		$xoffset	= 5;
+
+		foreach( $this->headerstrlen as $headername => &$headerlen ) {
+
+			$colorgreen	= imageColorAllocate( $this->image, 0, 200, 0 );
+
+			imageString( $this->image, $this->font, $xoffset, $this->y_offset, ucfirst( $headername ), $colorgreen );
+		
+			if( $headerlen < strlen( $headername ) )
+				$headerlen	= strlen( $headername );
+
+			$xoffset	= $xoffset + ($this->fontwidth * ( $headerlen + 1 ) );
+
+		}
+		$this->newLineOffset();
+	}
+
+	function newLineOffset() {
+
+		$this->y_offset		= $this->y_offset + $this->fontheight + $this->fontspaceing;
+	}
+
+	function draw() {
+
+		$xlen		= 450;
+		$ylen		= ( count( $this->njobs ) * ( $this->fontheight + $this->fontspaceing ) ) + (3 * $this->fontheight);
+
+		$this->image	= imageCreateTrueColor( $xlen, $ylen );
+		$colorwhite	= imageColorAllocate( $this->image, 255, 255, 255 );
+		imageFill( $this->image, 0, 0, $colorwhite );                         
+
+		$colorblue	= imageColorAllocate( $this->image, 0, 0, 255 );
+
+		imageString( $this->image, $this->font, 1, $this->y_offset, "Monarch Joblist - host: ".$this->host, $colorblue );
+		$this->newLineOffset();
+
+		$this->drawHeader();
+		$this->drawJobs();
+
+		header( 'Content-type: image/png' );
+		imagePNG( $this->image );
+		imageDestroy( $this->image );
+	}
+}
+
+function makeTime( $time ) {
+
+        $days = intval( $time / 86400 );
+        $time = ($days>0) ? $time % ($days * 86400) : $time;
+
+        //printf( "time = %s, days = %s\n", $time, $days );
+
+        $date_str = '';
+        $day_str = '';
+
+        if( $days > 0 ) {
+                if( $days > 1 )
+                        $day_str .= $days . ' days';
+                else
+                        $day_str .= $days . ' day';
+        }
+
+        $hours = intval( $time / 3600 );
+        $time = $hours ? $time % ($hours * 3600) : $time;
+
+        //printf( "time = %s, days = %s, hours = %s\n", $time, $days, $hours );
+        if( $hours > 0 ) {
+                $date_str .= $hours . ':';
+                $date_unit = 'hours'; 
+        }
+
+        $minutes = intval( $time / 60 );
+        $seconds = $minutes ? $time % ($minutes * 60) : $time;
+
+        if( $minutes > 0 ) {
+
+                if( $minutes >= 10 )
+                        $date_str .= $minutes . ':';
+                else
+                        $date_str .= '0' . $minutes . ':';
+
+                $date_unit = (!isset($date_unit)) ? 'minutes' : $date_unit;
+        } else {
+                if($hours > 0 ) {
+                        $date_str .= '00:';
+                        $date_unit = (!isset($date_unit)) ? 'minutes' : $date_unit;
+                }
+        }
+
+
+        $date_unit = (!isset($date_unit)) ? 'seconds' : $date_unit;
+
+        if( $seconds > 0 ) {
+
+                if( $seconds >= 10 )
+                        $date_str .= $seconds . ' ' . $date_unit;
+                else
+                        $date_str .= '0' . $seconds . ' ' . $date_unit;
+
+        } else if ( $hours > 0 or $minutes > 0 )
+
+                $date_str .= '00 ' . $date_unit;
+
+        if( $days > 0) {
+
+                if( $hours > 0 or $minutes > 0 or $seconds > 0 )
+                        $date_str = $day_str . ' - ' . $date_str;
+                else
+                        $date_str = $day_str;
+        }
+
+        return $date_str;
+}
+?>
Index: /tags/0.2/web/addons/job_monarch/overview.php
===================================================================
--- /tags/0.2/web/addons/job_monarch/overview.php	(revision 325)
+++ /tags/0.2/web/addons/job_monarch/overview.php	(revision 325)
@@ -0,0 +1,845 @@
+<?php
+/*
+ *
+ * This file is part of Jobmonarch
+ *
+ * Copyright (C) 2006  Ramon Bastiaans
+ *
+ * Jobmonarch 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 2 of the License, or
+ * (at your option) any later version.
+ *
+ * Jobmonarch 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, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
+ *
+ * SVN $Id$
+ */
+
+global $GANGLIA_PATH, $clustername, $tpl, $filter, $cluster, $get_metric_string, $cluster_url, $sh;
+global $hosts_up, $m, $start, $end, $filterorder, $COLUMN_REQUESTED_MEMORY, $COLUMN_QUEUED, $COLUMN_NODES, $hostname, $piefilter;
+
+$data_gatherer = new DataGatherer( $clustername );
+
+$tpl->assign( "clustername", $clustername );
+
+if( $JOB_ARCHIVE )
+	$tpl->assign( "cluster_url", rawurlencode($clustername) );
+
+$data_gatherer->parseXML();
+
+$heartbeat = $data_gatherer->getHeartbeat();
+$jobs = $data_gatherer->getJobs();
+$gnodes = $data_gatherer->getNodes();
+$cpus = $data_gatherer->getCpus();
+
+function setupFilterSettings() {
+	global $tpl, $filter, $clustername, $piefilter;
+
+	$filter_image_url = "";
+
+	$tpl->gotoBlock( "_ROOT" );
+
+	foreach( $filter as $filtername => $filtervalue ) {
+		$tpl->assign( "f_".$filtername, $filtervalue );
+		$filter_image_url .= "&$filtername=$filtervalue";
+	}
+
+	$tpl->assign( "clusterimage", "./image.php?c=".rawurlencode($clustername)."&view=big-clusterimage".$filter_image_url );
+	$tpl->assign( "f_order", $filterorder );
+
+	if( array_key_exists( "id", $filter ) )
+		$piefilter = 'id';
+	else if( array_key_exists( "user", $filter ) )
+		$piefilter = 'user';
+	else if( array_key_exists( "queue", $filter ) )
+		$piefilter = 'queue';
+
+	$pie = drawPie();
+	$tpl->assign("pie", $pie );
+}
+
+function timeToEpoch( $time ) {
+
+	$time_fields = explode( ':', $time );
+
+	if( count($time_fields) == 3 ) {
+
+		$hours = $time_fields[0];
+		$minutes = $time_fields[1];
+		$seconds = $time_fields[2];
+
+	} else if( count($time_fields) == 2 ) {
+
+		$hours = 0;
+		$minutes = $time_fields[0];
+		$seconds = $time_fields[1];
+
+	} else if( count($time_fields) == 1 ) {
+
+		$hours = 0;
+		$minutes = 0;
+		$seconds = $time_fields[0];
+	}
+
+	$myepoch = intval( $seconds + (intval( $minutes * 60 )) + (intval( $hours * 3600 )) );
+
+	return $myepoch;
+}
+
+//function makeDate( $time ) {
+//	return strftime( "%a %d %b %Y %H:%M:%S", $time );
+//}
+
+function colorRed( $color ) {
+	return substr( $color, 0, 2 );
+}
+function colorGreen( $color ) {
+	return substr( $color, 2, 2 );
+}
+function colorBlue( $color ) {
+	return substr( $color, 4, 2 );
+}
+
+function colorDiffer( $first, $second ) {
+
+	// Make sure these two colors differ atleast 50 R/G/B
+	$min_diff = 50;
+
+	$c1r = hexDec( colorRed( $first ) );
+	$c1g = hexDec( colorGreen( $first ) );
+	$c1b = hexDec( colorBlue( $first ) );
+
+	$c2r = hexDec( colorRed( $second ) );
+	$c2g = hexDec( colorGreen( $second ) );
+	$c2b = hexDec( colorBlue( $second ) );
+
+	$rdiff = ($c1r >= $c2r) ? $c1r - $c2r : $c2r - $c1r;
+	$gdiff = ($c1g >= $c2g) ? $c1g - $c2g : $c2g - $c1g;
+	$bdiff = ($c1b >= $c2b) ? $c1b - $c2b : $c2b - $c1b;
+
+	if( $rdiff >= $min_diff or $gdiff >= $min_diff or $bdiff >= $min_diff )
+		return TRUE;
+	else
+		return FALSE;
+}
+
+function randomColor( $known_colors ) {
+
+	$start = "004E00";
+	
+	$start_red = colorRed( $start );
+	$start_green = colorGreen( $start );
+	$start_blue = colorBlue( $start );
+	
+	$end = "FFFFFF";
+
+	$end_red = colorRed( $end );
+	$end_green = colorGreen( $end );
+	$end_blue = colorBlue( $end );
+
+	$change_color = TRUE;
+
+	while( $change_color ) {
+
+		$change_color = FALSE;
+
+		$new_red = rand( hexDec( $start_red ), hexDec( $end_red ) );
+		$new_green = rand( hexDec( $start_green ), hexDec( $end_green ) );
+		$new_blue = rand( hexDec( $start_blue ), hexDec( $end_blue ) );
+
+		$new = decHex( $new_red ) . decHex( $new_green ) . decHex( $new_blue );
+
+		foreach( $known_colors as $old )
+
+			if( !colorDiffer( $new, $old ) )
+
+	 			$change_color = TRUE;
+	}
+
+	// Whoa! Actually found a good color ;)
+	return $new;
+}
+
+function drawJobPie() {
+}
+
+function drawUserPie() {
+
+}
+
+function drawQueuePie() {
+
+}
+
+
+function drawPie() {
+
+	global $jobs, $gnodes, $piefilter, $filter, $metrics;
+
+	$nodes 		= $gnodes;
+
+	if( isset($piefilter) )	
+		$pie_args	= "title=" . rawurlencode("Cluster ".$piefilter." usage");
+	else
+		$pie_args 	= "title=" . rawurlencode("Cluster queue usage");
+		
+	$pie_args 	.= "&size=250x150";
+
+	$queues 	= array();
+	$nr_jobs 	= count( $jobs );
+	$nr_nodes 	= count( $nodes );
+
+	$nr_cpus 	= cluster_sum("cpu_num", $metrics);
+
+	$empty_cpus 	= 0;
+	$used_cpus 	= 0;
+
+	$job_weight 	= array();
+
+	foreach( $nodes as $node ) {
+
+		$myjobs		= $node->getJobs();
+		$myhost		= $node->getHostname();
+		$node_cpus	= $metrics[$myhost]["cpu_num"][VAL];
+		$job_cpu	= 0;
+
+		foreach( $myjobs as $myjob ) {
+
+			$job_cpu	+= (int) $jobs[$myjob][ppn] ? $jobs[$myjob][ppn] : 1;
+		}
+
+		$node_freecpu	= $node_cpus - $job_cpu;
+
+		$empty_cpus	+= $node_freecpu;
+	}
+
+	$used_cpus		= $nr_cpus - $empty_cpus;
+
+	$empty_percentage 	= ($empty_cpus / $nr_cpus) * 100;
+
+	$qcolors 		= array();
+	$color 			= randomColor( $qcolors );
+	$qcolors[] 		= $color;
+	$pie_args 		.= "&free=$empty_percentage,$color";
+
+	if( isset( $piefilter ) )
+		$filterpie = array();
+
+	foreach( $nodes as $node ) {
+
+		$node_jobs 	= $node->getJobs();
+		$nr_node_jobs 	= count( $node_jobs );
+		$myhost 	= $node->getHostname();
+		$node_cpus	= $metrics[$myhost]["cpu_num"][VAL];
+
+		foreach( $node_jobs as $myjob ) {
+
+			$job_cpu		= (int) $jobs[$myjob][ppn] ? $jobs[$myjob][ppn] : 1;
+
+			// Determine the weight of this job
+			// - what percentage of the cpus is in use by this job
+			//
+			$job_weight[$myjob]	= ( $job_cpu / $nr_cpus );
+
+
+			if( isset($piefilter) ) {
+				$countjob = 1;
+				if( $piefilter == 'id' ) {
+					if( $myjob != $filter[$piefilter] )
+						$countjob = 0;
+				} else if( $piefilter == 'user' ) {
+					if( $jobs[$myjob][owner] != $filter[$piefilter] )
+						$countjob = 0;
+				} else {
+					if( $jobs[$myjob][$piefilter] != $filter[$piefilter] )
+						$countjob = 0;
+				}
+
+				if( $countjob ) {
+
+					if( !isset( $filterpie[$filter[$piefilter]] ) )
+						$filterpie[$filter[$piefilter]] = $job_weight[$myjob];
+					else
+						$filterpie[$filter[$piefilter]] = $filterpie[$filter[$piefilter]] + $job_weight[$myjob];
+				} else {
+					if( !isset( $filterpie["other"] ) )
+						$filterpie["other"] = $job_weight[$myjob];
+					else
+						$filterpie["other"] = $filterpie["other"] + $job_weight[$myjob];
+
+				}
+				
+			} else {
+
+				$qname		= $jobs[$myjob][queue];
+
+				if( !isset( $queues[$qname] ) )
+					$queues[$qname] = $job_weight[$myjob];
+				else
+					$queues[$qname] = $queues[$qname] + $job_weight[$myjob];
+			}
+		}
+	}
+
+	if( isset( $piefilter ) )
+		$graphvals = $filterpie;
+	else
+		$graphvals = $queues;
+
+	foreach( $graphvals as $name => $totalweight) {
+
+		$percentage 	= ( $totalweight * 100 );
+		
+		$color 		= randomColor( $qcolors );
+		$qcolors[] 	= $color;
+		$pie_args 	.= "&$name=$percentage,$color";
+	}
+	$pie = "../../pie.php?$pie_args";
+
+	return $pie;
+}
+
+
+function sortJobs( $jobs, $sortby, $sortorder ) {
+
+	$sorted = array();
+
+	$cmp = create_function( '$a, $b', 
+		"global \$sortby, \$sortorder;".
+
+		"if( \$a == \$b ) return 0;".
+
+		"if (\$sortorder==\"desc\")".
+			"return ( \$a < \$b ) ? 1 : -1;".
+		"else if (\$sortorder==\"asc\")".
+			"return ( \$a > \$b ) ? 1 : -1;" );
+
+	if( isset( $jobs ) && count( $jobs ) > 0 ) {
+
+		foreach( $jobs as $jobid => $jobattrs ) {
+
+				$state = $jobattrs[status];
+				$user = $jobattrs[owner];
+				$queue = $jobattrs[queue];
+				$name = $jobattrs[name];
+				$req_cpu = $jobattrs[requested_time];
+				$req_memory = $jobattrs[requested_memory];
+
+				if( $state == 'R' )
+					$nodes = count( $jobattrs[nodes] );
+				else
+					$nodes = $jobattrs[nodes];
+
+				$ppn = (int) $jobattrs[ppn] ? $jobattrs[ppn] : 1;
+				$cpus = $nodes * $ppn;
+				$queued_time = (int) $jobattrs[queued_timestamp];
+				$start_time = (int) $jobattrs[start_timestamp];
+				$runningtime = $report_time - $start_time;
+
+				switch( $sortby ) {
+					case "id":
+						$sorted[$jobid] = $jobid;
+						break;
+
+					case "state":
+						$sorted[$jobid] = $state;
+						break;
+
+					case "user":
+						$sorted[$jobid] = $user;
+						break;
+
+					case "queue":
+						$sorted[$jobid] = $queue;
+						break;
+
+					case "name":
+						$sorted[$jobid] = $name;
+						break;
+
+					case "req_cpu":
+						$sorted[$jobid] = timeToEpoch( $req_cpu );
+						break;
+
+					case "req_mem":
+						$sorted[$jobid] = $req_memory;
+						break;
+
+					case "nodes":
+						$sorted[$jobid] = $nodes;
+						break;
+
+					case "cpus":
+						$sorted[$jobid] = $cpus;
+						break;
+
+					case "queued":
+						$sorted[$jobid] = $queued_time;
+						break;
+
+					case "start":
+						$sorted[$jobid] = $start_time;
+						break;
+
+					case "runningtime":
+						$sorted[$jobid] = $runningtime;
+						break;
+
+					default:
+						break;
+
+				}
+		}
+	}
+
+	//uasort( $sorted, $cmp );
+	if( $sortorder == "asc" )
+		arsort( $sorted );
+	else if( $sortorder == "desc" )
+		asort( $sorted );
+
+	return $sorted;
+}
+
+function makeOverview() {
+
+	global $jobs, $nodes, $heartbeat, $clustername, $tpl;
+	global $sortorder, $sortby, $filter, $sh, $hc, $m;
+	global $cluster_url, $get_metric_string, $host_url, $metrics;
+	global $start, $end, $reports, $gnodes, $default_showhosts;
+	global $COLUMN_QUEUED, $COLUMN_REQUESTED_MEMORY, $COLUMN_NODES, $hostname;
+	$metricname = $m;
+
+	$tpl->assign("sortorder", $sortorder );
+	$tpl->assign("sortby", $sortby );
+
+	$sorted_jobs = sortJobs( $jobs, $sortby, $sortorder );
+
+	$even = 1;
+
+	$used_jobs = 0;
+	$used_cpus = 0;
+	$used_nodes = 0;
+
+	$queued_jobs = 0;
+	$queued_nodes = 0;
+	$queued_cpus = 0;
+
+	$total_nodes = 0;
+	$total_cpus = 0;
+	$total_jobs = 0;
+
+	$all_used_nodes = array();
+	$total_used_nodes = array();
+
+	$running_name_nodes = array();
+
+	$running_nodes = 0;
+	$running_jobs = 0;
+	$running_cpus = 0;
+
+	$avail_nodes = count( $gnodes );
+	$avail_cpus = cluster_sum("cpu_num", $metrics);
+
+	$view_cpus = 0;
+	$view_jobs = 0;
+	$view_nodes = 0;
+
+	$all_nodes = 0;
+	$all_jobs = 0;
+	$all_cpus = 0;
+
+	$view_name_nodes = array();
+
+	if( $COLUMN_REQUESTED_MEMORY ) {
+		$tpl->newBlock( "column_header_req_mem" );
+	}
+
+	if( $COLUMN_NODES ) {
+		$tpl->newBlock( "column_header_nodes" );
+	}
+
+	if( $COLUMN_QUEUED ) {
+		$tpl->newBlock( "column_header_queued" );
+	}
+
+	$last_displayed_job = null;
+
+	foreach( $sorted_jobs as $jobid => $sortdec ) {
+
+		$report_time = $jobs[$jobid][reported];
+
+		if( $jobs[$jobid][status] == 'R' )
+			$nodes = count( $jobs[$jobid][nodes] );
+		else if( $jobs[$jobid][status] == 'Q' )
+			$nodes = $jobs[$jobid][nodes];
+
+		$ppn = (int) $jobs[$jobid][ppn] ? $jobs[$jobid][ppn] : 1;
+		$cpus = $nodes * $ppn;
+
+		if( $report_time == $heartbeat ) {
+
+			$display_job = 1;
+
+			if( $jobs[$jobid][status] == 'R' ) {
+				foreach( $jobs[$jobid][nodes] as $tempnode ) {
+					$all_used_nodes[] = $tempnode;
+				}
+			}
+
+			$used_cpus += $cpus;
+
+			if( $jobs[$jobid][status] == 'R' ) {
+				$running_cpus += $cpus;
+				$running_jobs++;
+
+				$found_node_job = 0;
+
+				foreach( $jobs[$jobid][nodes] as $tempnode ) {
+					$running_name_nodes[] = $tempnode;
+
+					if( isset( $hostname ) && $hostname != '' ) {
+						//$filter[host] = $hostname;
+
+						$domain_len = 0 - strlen( $jobs[$jobid][domain] );
+						$hostnode = $tempnode;
+						if( substr( $hostnode, $domain_len ) != $jobs[$jobid][domain] ) {
+							$hostnode = $hostnode. '.'. $jobs[$jobid][domain];
+						}
+
+						if( $hostname == $hostnode ) {
+							$found_node_job = 1;
+							$display_job = 1;
+						} else if( !$found_node_job ) {
+							$display_job = 0;
+						}
+					}
+				}
+			}
+
+			if( $jobs[$jobid][status] == 'Q' ) {
+				if( isset( $hostname ) && $hostname != '' )
+					$display_job = 0;
+
+				$queued_cpus += $cpus;
+				$queued_nodes += $nodes;
+				$queued_jobs++;
+			}
+
+			foreach( $filter as $filtername=>$filtervalue ) {
+
+				if( $filtername == 'id' && $jobid != $filtervalue )
+					$display_job = 0;
+				else if( $filtername == 'state' && $jobs[$jobid][status] != $filtervalue )
+					$display_job = 0;
+				else if( $filtername == 'queue' && $jobs[$jobid][queue] != $filtervalue )
+					$display_job = 0;
+				else if( $filtername == 'user' && $jobs[$jobid][owner] != $filtervalue )
+					$display_job = 0;
+			}
+
+			if( $display_job ) {
+
+				$tpl->newBlock("node");
+				$tpl->assign( "clustername", $clustername );
+				$tpl->assign("id", $jobid );
+
+				$last_displayed_job = $jobid;
+
+				$tpl->assign("state", $jobs[$jobid][status] );
+
+				$fullstate = '';
+				if( $jobs[$jobid][status] == 'R' ) {
+					$fullstate = "Running";
+				} else if( $jobs[$jobid][status] == 'Q' ) {
+					$fullstate = "Queued";
+				}
+
+				$tpl->assign("fullstate", $fullstate );
+				
+				$tpl->assign("user", $jobs[$jobid][owner] );
+				$tpl->assign("queue", $jobs[$jobid][queue] );
+
+				$fulljobname = $jobs[$jobid][name];
+				$shortjobname = '';
+
+				$tpl->assign("fulljobname", $fulljobname );
+
+				$fulljobname_fields	= explode( ' ', $fulljobname );
+
+				$capjobname	= 0;
+
+				if( strlen( $fulljobname_fields[0] ) > 10 )
+					$capjobname	= 1;
+
+				if( $capjobname ) {
+					$tpl->newBlock("jobname_hint_start");
+					$tpl->gotoBlock("node");
+
+					$shortjobname = substr( $fulljobname, 0, 10 ) . '..';
+				} else {
+					$shortjobname = $fulljobname;
+				}
+				
+				$tpl->assign("name", $shortjobname );
+
+				if( $capjobname ) {
+					$tpl->newBlock("jobname_hint_end");
+					$tpl->gotoBlock("node");
+				}
+
+				$domain = $jobs[$jobid][domain];
+				$tpl->assign("req_cpu", makeTime( timeToEpoch( $jobs[$jobid][requested_time] ) ) );
+
+				if( $COLUMN_REQUESTED_MEMORY ) {
+					$tpl->newBlock( "column_req_mem" );
+					$tpl->assign( "req_memory", $jobs[$jobid][requested_memory] );
+					$tpl->gotoBlock( "node" );
+				}
+
+
+				if( $COLUMN_QUEUED ) {
+					$tpl->newBlock( "column_queued" );
+					$tpl->assign( "queued", makeDate( $jobs[$jobid][queued_timestamp] ) );
+					$tpl->gotoBlock( "node" );
+				}
+				if( $COLUMN_NODES ) {
+					$tpl->newBlock( "column_nodes" );
+					$tpl->gotoBlock( "node" );
+				}
+
+				$ppn = (int) $jobs[$jobid][ppn] ? $jobs[$jobid][ppn] : 1;
+				$cpus = $nodes * $ppn;
+				$tpl->assign("nodes", $nodes );
+				$tpl->assign("cpus", $cpus );
+				$start_time = (int) $jobs[$jobid][start_timestamp];
+				$job_start = $start_time;
+
+				$view_cpus += $cpus;
+				$view_jobs++;
+
+				if( $jobs[$jobid][status] == 'R' ) {
+					foreach( $jobs[$jobid][nodes] as $tempnode )
+						$view_name_nodes[] = $tempnode;
+
+					if( $COLUMN_NODES ) {
+						$tpl->gotoBlock( "column_nodes" );
+						$mynodehosts = array();
+						foreach( $jobs[$jobid][nodes] as $mynode ) {
+							$myhost_href = "./?c=".$clustername."&h=".$mynode.".".$jobs[$jobid][domain];
+							$mynodehosts[] = "<A HREF=\"".$myhost_href."\">".$mynode."</A>";
+						}
+						$nodes_hostnames = implode( " ", $mynodehosts );
+						$tpl->assign( "nodes_hostnames", $nodes_hostnames );
+						$tpl->gotoBlock( "node" );
+					}
+				} else if( $jobs[$jobid][status] == 'Q' ) {
+					$view_nodes += (int) $jobs[$jobid][nodes];
+				}
+
+				if( $even ) {
+
+					$tpl->assign("nodeclass", "even");
+					$even = 0;
+				} else {
+
+					$tpl->assign("nodeclass", "odd");
+					$even = 1;
+				}
+
+				if( $start_time ) {
+
+					$runningtime = makeTime( $report_time - $start_time );
+					$job_runningtime = $report_time - $start_time;
+					$tpl->assign("started", makeDate( $start_time ) );
+					$tpl->assign("runningtime", $runningtime );
+				}
+			}
+		}
+	}
+	$all_used_nodes = array_unique( $all_used_nodes );
+	$view_name_nodes = array_unique( $view_name_nodes );
+	$running_name_nodes = array_unique( $running_name_nodes );
+
+	$used_nodes = count( $all_used_nodes );
+	$view_nodes += count( $view_name_nodes );
+	$running_nodes += count( $running_name_nodes );
+
+	$total_nodes = $queued_nodes + $running_nodes;
+	$total_cpus = $queued_cpus + $running_cpus;
+	$total_jobs = $queued_jobs + $running_jobs;
+
+	$tpl->assignGlobal("avail_nodes", $avail_nodes );
+	$tpl->assignGlobal("avail_cpus", $avail_cpus );
+
+	$tpl->assignGlobal("queued_nodes", $queued_nodes );
+	$tpl->assignGlobal("queued_jobs", $queued_jobs );
+	$tpl->assignGlobal("queued_cpus", $queued_cpus );
+
+	$tpl->assignGlobal("total_nodes", $total_nodes );
+	$tpl->assignGlobal("total_jobs", $total_jobs );
+	$tpl->assignGlobal("total_cpus", $total_cpus );
+
+	$tpl->assignGlobal("running_nodes", $running_nodes );
+	$tpl->assignGlobal("running_jobs", $running_jobs );
+	$tpl->assignGlobal("running_cpus", $running_cpus );
+
+	$tpl->assignGlobal("used_nodes", $used_nodes );
+	$tpl->assignGlobal("used_jobs", $used_jobs );
+	$tpl->assignGlobal("used_cpus", $used_cpus );
+
+	$free_nodes = $avail_nodes - $running_nodes;
+	$free_cpus = $avail_cpus - $running_cpus;
+
+	$tpl->assignGlobal("free_nodes", $free_nodes );
+	$tpl->assignGlobal("free_cpus", $free_cpus );
+
+	$tpl->assignGlobal("view_nodes", $view_nodes );
+	$tpl->assignGlobal("view_jobs", $view_jobs );
+	$tpl->assignGlobal("view_cpus", $view_cpus );
+
+	$tpl->assignGlobal("report_time", makeDate( $heartbeat));
+
+	if( intval($view_jobs) == 1 and $start_time )
+		if( $last_displayed_job != null )
+			$filter[id] = $last_displayed_job;
+
+	makeHeader( 'overview' );
+	setupFilterSettings();
+
+	if( intval($view_jobs) == 1 and $start_time ) {
+
+		$tpl->newBlock( "showhosts" );
+
+		# Present a width list
+		$cols_menu = "<SELECT NAME=\"hc\" OnChange=\"toga_form.submit();\">\n";
+
+
+		$hostcols = ($hc) ? $hc : 4;
+
+		foreach(range(1,25) as $cols) {
+			$cols_menu .= "<OPTION VALUE=$cols ";
+			if ($cols == $hostcols)
+				$cols_menu .= "SELECTED";
+			$cols_menu .= ">$cols\n";
+		}
+		$cols_menu .= "</SELECT>\n";
+
+		//$tpl->assign("cluster", $clustername);
+		$tpl->assign("metric","$metricname $units");
+		$tpl->assign("id", $filter[id]);
+		# Host columns menu defined in header.php
+		$tpl->assign("cols_menu", $cols_menu);
+
+		$showhosts = isset($sh) ? $sh : $default_showhosts;
+		//if( !$showhosts) $showhosts = $default_showhosts;
+		$tpl->assign("checked$showhosts", "checked");
+
+		if( $showhosts ) {
+			//-----
+
+			if( !isset($start) ) $start="jobstart";
+			if( !isset($stop) ) $stop="now";
+			//$tpl->assign("start", $start);
+			//$tpl->assign("stop", $stop);
+
+			$sorted_hosts = array();
+			$hosts_up = $jobs[$filter[id]][nodes];
+
+			$r = intval($job_runningtime * 1.25);
+
+			$jobrange = ($job_runningtime < 3600) ? -3600 : -$r ;
+			$jobstart = $report_time - $job_runningtime;
+
+			if ($reports[$metricname])
+				$metricval = "g";
+			else
+				$metricval = "m";
+				
+			foreach ($hosts_up as $host ) {
+
+				$domain_len = 0 - strlen( $domain );
+				if( substr( $host, $domain_len ) != $domain ) {
+					$host = $host. '.'.$domain;
+				}
+				$cpus = $metrics[$host]["cpu_num"][VAL];
+				if (!$cpus) $cpus=1;
+				$load_one  = $metrics[$host]["load_one"][VAL];
+				$load = ((float) $load_one)/$cpus;
+				$host_load[$host] = $load;
+				$percent_hosts[load_color($load)] += 1;
+				if ($metricname=="load_one")
+					$sorted_hosts[$host] = $load;
+				else
+					$sorted_hosts[$host] = $metrics[$host][$metricname][VAL];
+			}
+			switch ($sort) {
+				case "descending":
+					arsort($sorted_hosts);
+					break;
+				case "by hostname":
+					ksort($sorted_hosts);
+					break;
+				default:
+				case "ascending":
+					asort($sorted_hosts);
+					break;
+			}
+
+			//$sorted_hosts = array_merge($down_hosts, $sorted_hosts);
+
+			# First pass to find the max value in all graphs for this
+			# metric. The $start,$end variables comes from get_context.php,
+			# included in index.php.
+			list($min, $max) = find_limits($sorted_hosts, $metricname);
+
+			# Second pass to output the graphs or metrics.
+			$i = 1;
+			foreach ( $sorted_hosts as $host=>$value  ) {
+				$tpl->newBlock ("sorted_list");
+				//$host = $host. '.'.$domain;
+				$host_url = rawurlencode($host);
+				$cluster_url = rawurlencode($clustername);
+
+				$textval = "";
+				//printf("host = %s, value = %s", $host, $value);
+				//echo "$host: $value, ";
+				$val = $metrics[$host][$metricname];
+				$class = "metric";
+				$host_link="\"../../?c=$cluster_url&h=$host_url&r=job&jr=$jobrange&js=$jobstart\"";
+
+				if ($val[TYPE]=="timestamp" or $always_timestamp[$metricname]) {
+					$textval = date("r", $val[VAL]);
+				} elseif ($val[TYPE]=="string" or $val[SLOPE]=="zero" or $always_constant[$metricname] or ($max_graphs > 0 and $i > $max_graphs )) {
+					$textval = "$val[VAL] $val[UNITS]";
+				} else {
+					$load_color = load_color($host_load[$host]);
+					$graphargs = ($reports[$metricname]) ? "g=$metricname&" : "m=$metricname&";
+					$graphargs .= "z=small&c=$cluster_url&h=$host_url&l=$load_color" ."&v=$val[VAL]&r=job&jr=$jobrange&js=$jobstart";
+					if( $max > 0 ) {
+
+						$graphargs .= "&x=$max&n=$min";
+					}
+				}
+				if ($textval) {
+					$cell="<td class=$class>".  "<b><a href=$host_link>$host</a></b><br>".  "<i>$metricname:</i> <b>$textval</b></td>";
+				} else {
+					$cell="<td><a href=$host_link>".  "<img src=\"../../graph.php?$graphargs\" ".  "alt=\"$host\" border=0></a></td>";
+				}
+
+				$tpl->assign("metric_image", $cell);
+				if (! ($i++ % $hostcols) )
+					 $tpl->assign ("br", "</tr><tr>");
+			}
+		}
+//---
+	}
+}
+?>
Index: /tags/0.2/web/addons/job_monarch/search.php
===================================================================
--- /tags/0.2/web/addons/job_monarch/search.php	(revision 325)
+++ /tags/0.2/web/addons/job_monarch/search.php	(revision 325)
@@ -0,0 +1,526 @@
+<?php
+/*
+ *
+ * This file is part of Jobmonarch
+ *
+ * Copyright (C) 2006  Ramon Bastiaans
+ *
+ * Jobmonarch 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 2 of the License, or
+ * (at your option) any later version.
+ *
+ * Jobmonarch 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, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
+ *
+ * SVN $Id$
+ */
+
+global $clustername, $tpl;
+
+function validateFormInput() {
+	global $clustername, $tpl, $id, $user, $name, $start_from_time, $start_to_time, $queue;
+	global $end_from_time, $end_to_time, $period_start, $period_stop;
+
+	$error = 0;
+	$error_msg = "<FONT COLOR=\"red\"><B>";
+	$show_msg = 0;
+
+	$none_set = 0;
+
+	if( $id == '' and $user == '' and $name == '' and $start_from_time == '' and $start_to_time == '' and $queue == '' and $end_from_time == '' and $end_to_time == '') {
+		$error = 1;
+		$show_msg = 1;
+		$error_msg .= "No search criteria set!";
+	}
+
+	if( !is_numeric($id) and !$error and $id != '') {
+
+		$error = 1;
+		$show_msg = 1;
+		$error_msg .= "Id must be a number";
+	}
+
+	//printf( "period_start = %s period_stop = %s\n", $period_start, $period_stop );
+
+	if( !$error and $period_start != '' ) {
+		//printf( "period_start = %s period_stop = %s\n", $period_start, $period_stop );
+		$pstart_epoch = datetimeToEpoch( $period_start );
+		//printf( "period_start = %s period_stop = %s\n", $period_start, $period_stop );
+		if( $period_stop != '' ) {
+
+			$pstop_epoch = datetimeToEpoch( $period_stop );
+			//printf( "pstop_epoch = %s pstart_epoch = %s\n", $pstop_epoch, $pstart_epoch );
+
+			if( $pstart_epoch > $pstop_epoch ) {
+
+				$show_msg = 1;
+				$error_msg .= "Graph timeperiod reset: start date/time can't be later than end";
+				$period_stop = '';
+				$period_start = '';
+			} else if( $pstop_epoch == $pstart_epoch ) {
+
+				$show_msg = 1;
+				$error_msg .= "Graph timeperiod reset: start and end date/time can't be the same";
+				$period_stop = '';
+				$period_start = '';
+			}
+		}
+	}
+
+	$error_msg .= "</B></FONT>";
+	// doe checks en set error en error_msg in case shit
+
+	if( $show_msg )
+		$tpl->assign( "form_error_msg", $error_msg );
+
+	return ($error ? 0 : 1 );
+}
+
+//function makeDate( $time ) {
+//        return strftime( "%a %d %b %Y %H:%M:%S", $time );
+//}
+
+function datetimeToEpoch( $datetime ) {
+
+	//printf("datetime = %s\n", $datetime );
+	$datetime_fields = explode( ' ', $datetime );
+
+	$date = $datetime_fields[0];
+	$time = $datetime_fields[1];
+
+	$date_fields = explode( '-', $date );
+
+	$days = $date_fields[0];
+	$months = $date_fields[1];
+	$years = $date_fields[2];
+
+	//printf( "days = %s months = %s years = %s\n", $days, $months, $years );
+
+	$time_fields = explode( ':', $time );
+
+	$hours = $time_fields[0];
+	$minutes = $time_fields[1];
+	$seconds = $time_fields[2];
+
+	//printf( "hours = %s minutes = %s seconds = %s\n", $hours, $minutes, $seconds );
+
+	$timestamp = mktime( $hours, $minutes, $seconds, $months, $days, $years );
+
+	//printf( "timestamp = %s\n", $timestamp );
+
+	return $timestamp;
+}
+
+function timeToEpoch( $time ) {
+
+        $time_fields = explode( ':', $time );
+
+        if( count($time_fields) == 3 ) {
+
+                $hours = $time_fields[0];
+                $minutes = $time_fields[1];
+                $seconds = $time_fields[2];
+
+        } else if( count($time_fields) == 2 ) {
+
+                $hours = 0;
+                $minutes = $time_fields[0];
+                $seconds = $time_fields[1];
+
+        } else if( count($time_fields) == 1 ) {
+
+                $hours = 0;
+                $minutes = 0;
+                $seconds = $time_fields[0];
+        }
+
+        $myepoch = intval( $seconds + (intval( $minutes * 60 )) + (intval( $hours * 3600 )) );
+
+        return $myepoch;
+}
+
+function sortJobs( $jobs, $nodes, $sortby, $sortorder ) {
+
+	//printf("sortby = %s sortorder = %s\n", $sortby, $sortorder );
+
+        $sorted = array();
+
+        $cmp = create_function( '$a, $b',
+                "global \$sortby, \$sortorder;".
+
+                "if( \$a == \$b ) return 0;".
+
+                "if (\$sortorder==\"desc\")".
+                        "return ( \$a < \$b ) ? 1 : -1;".
+                "else if (\$sortorder==\"asc\")".
+                        "return ( \$a > \$b ) ? 1 : -1;" );
+
+	//print_r( $jobs );
+
+        foreach( $jobs as $jobid => $jobattrs ) {
+
+                        $state = $jobattrs[status];
+                        $user = $jobattrs[owner];
+                        $queue = $jobattrs[queue];
+                        $name = $jobattrs[name];
+                        $req_cpu = $jobattrs[requested_time];
+                        $req_memory = $jobattrs[requested_memory];
+
+                        //if( $state == 'R' )
+			$mynodes = count( $nodes[$jobid] );
+                        //else
+                        //        $nodes = $jobattrs[nodes];
+
+                        $ppn = (int) $jobattrs[ppn] ? $jobattrs[ppn] : 1;
+                        $cpus = $mynodes * $ppn;
+                        $start_time = (int) $jobattrs[start_timestamp];
+                        $stop_time = (int) $jobattrs[stop_timestamp];
+                        $runningtime = $stop_time - $start_time;
+
+                        switch( $sortby ) {
+                                case "id":
+                                        $sorted[$jobid] = $jobid;
+                                        break;
+
+                                case "state":
+                                        $sorted[$jobid] = $state;
+                                        break;
+
+                                case "user":
+                                        $sorted[$jobid] = $user;
+                                        break;
+
+                                case "queue":
+                                        $sorted[$jobid] = $queue;
+                                        break;
+
+                                case "name":
+                                        $sorted[$jobid] = $name;
+                                        break;
+
+                                case "req_cpu":
+                                        $sorted[$jobid] = timeToEpoch( $req_cpu );
+                                        break;
+
+                                case "req_mem":
+                                        $sorted[$jobid] = $req_memory;
+                                        break;
+
+                                case "nodes":
+                                        $sorted[$jobid] = $mynodes;
+                                        break;
+
+                                case "cpus":
+                                        $sorted[$jobid] = $cpus;
+                                        break;
+
+                                case "start":
+                                        $sorted[$jobid] = $start_time;
+                                        break;
+
+				case "finished":
+					$sorted[$jobid] = $stop_time;
+					break;
+
+                                case "runningtime":
+                                        $sorted[$jobid] = $runningtime;
+                                        break;
+
+                                default:
+                                        break;
+
+                        }
+        }
+
+        //uasort( $sorted, $cmp );
+        if( $sortorder == "asc" )
+                arsort( $sorted );
+        else if( $sortorder == "desc" )
+                asort( $sorted );
+
+	//print_r( $sorted );
+
+        return array_keys( $sorted );
+}
+
+function makeSearchPage() {
+	global $clustername, $tpl, $id, $user, $name, $start_from_time, $start_to_time, $queue;
+	global $end_from_time, $end_to_time, $filter, $default_showhosts, $m, $hosts_up, $hc;
+	global $period_start, $period_stop, $sortby, $sortorder, $COLUMN_REQUESTED_MEMORY;
+	global $SEARCH_RESULT_LIMIT, $COLUMN_NODES;
+
+	$metricname = $m;
+	//printf("job_start = %s job_stop = %s\n", $job_start, $job_stop );
+	//printf("start = %s stop = %s\n", $start, $stop );
+
+	$tpl->assign( "cluster", $clustername );
+	$tpl->assign( "id_value", $id );
+	$tpl->assign( "user_value", $user );
+	$tpl->assign( "queue_value", $queue );
+	$tpl->assign( "name_value", $name );
+	$tpl->assign( "start_from_value", rawurldecode( $start_from_time ) );
+	$tpl->assign( "start_to_value", rawurldecode( $start_to_time ) );
+	$tpl->assign( "end_from_value", rawurldecode( $end_from_time ) );
+	$tpl->assign( "end_to_value", rawurldecode( $end_to_time ) );
+
+	if( validateFormInput() ) {
+
+		$tpl->newBlock( "search_results" );
+		$tpl->assign( "sortby", $sortby);
+		$tpl->assign( "sortorder", $sortorder);
+		$tdb = new TarchDbase( "127.0.0.1" );
+		if( $start_from_time ) $start_from_time = datetimeToEpoch( $start_from_time );
+		if( $start_to_time ) $start_to_time = datetimeToEpoch( $start_to_time );
+		if( $end_from_time ) $end_from_time = datetimeToEpoch( $end_from_time );
+		if( $end_to_time ) $end_to_time = datetimeToEpoch( $end_to_time );
+		$search_ids = $tdb->searchDbase( $id, $queue, $user, $name, $start_from_time, $start_to_time, $end_from_time, $end_to_time );
+
+		if( ($tdb->resultcount) > (int) $SEARCH_RESULT_LIMIT ) {
+			$tpl->gotoBlock( "_ROOT" );
+		
+			$tpl->assign( "form_error_msg", "Got " . $tdb->resultcount . " search results, output limited to last " . $SEARCH_RESULT_LIMIT . " jobs." );
+			$tpl->gotoBlock( "search_results" );
+		}
+
+		$jobs = array();
+		$nodes = array();
+
+		$even = 1;
+
+		//print_r( $search_ids );
+
+		foreach( $search_ids as $myid ) {
+
+			//printf( "myid %s\n", $myid );
+			$jobs[$myid] = $tdb->getJobArray( $myid );
+			$nodes[$myid] = $tdb->getNodesForJob( $myid );
+		}
+
+		if( $COLUMN_REQUESTED_MEMORY ) {
+			$tpl->newBlock( "column_header_req_mem" );
+		}
+		if( $COLUMN_NODES ) {
+			$tpl->newBlock( "column_header_nodes" );
+		}
+
+		//print_r( $nodes );
+		$sorted_search = sortJobs( $jobs, $nodes, $sortby, $sortorder );
+
+		//print_r( $sorted_search );
+		foreach( $sorted_search as $sortid ) {
+
+			$job = $jobs[$sortid];
+			//print_r( $job );
+			$foundid = $job[id];
+			//printf( "foundid %s\n", $foundid );
+
+			//$job = $tdb->getJobArray( $foundid );
+			//$nodes = $tdb->getNodesForJob( $foundid );
+
+			$tpl->newBlock( "node" );
+			$tpl->assign( "id", $job[id] );
+			$tpl->assign( "state", $job[status] );
+			$tpl->assign( "user", $job[owner] );
+			$tpl->assign( "queue", $job[queue] );
+			$tpl->assign( "name", $job[name] );
+			$tpl->assign( "req_cpu", makeTime( TimeToEpoch( $job[requested_time] ) ) );
+
+			if( $COLUMN_REQUESTED_MEMORY ) {
+				$tpl->newBlock( "column_req_mem" );
+				//$tpl->assign( "req_memory", $jobs[$jobid][requested_memory] );
+				$tpl->assign( "req_memory", $job[requested_memory] );
+				$tpl->gotoBlock( "node" );
+			}
+			if( $COLUMN_NODES) {
+
+				foreach( $nodes[$foundid] as $mynode )
+					$job_nodes[] = $mynode[hostname];
+
+				$tpl->newBlock( "column_nodes" );
+				$nodes_hostnames = implode( " ", $job_nodes );
+				$tpl->assign( "nodes_hostnames", $nodes_hostnames );
+				$tpl->gotoBlock( "node" );
+			}
+
+			$nodes_nr = count( $nodes[$foundid] );
+
+			if( $even ) {
+
+				$tpl->assign("nodeclass", "even");
+				$even = 0;
+			} else {
+
+				$tpl->assign("nodeclass", "odd");
+				$even = 1;
+			}
+
+
+			// need to replace later with domain stored from dbase
+			//
+			//$job_domain = $job[domain];
+
+			//$myhost = $_SERVER[HTTP_HOST];
+			//$myhf = explode( '.', $myhost );
+			//$myhf = array_reverse( $myhf );
+			//array_pop( $myhf );
+			//$myhf = array_reverse( $myhf );
+			//$job_domain = implode( '.', $myhf );
+			
+			//print_r( $job );
+			//printf( "job domain = %s\n", $job_domain);
+			$ppn = (int) $job[ppn] ? $job[ppn] : 1;
+			$cpus = $nodes_nr * $ppn;
+
+			$tpl->assign( "nodes", $nodes_nr );
+			$tpl->assign( "cpus", $cpus );
+
+			$job_start = $job[start_timestamp];
+			$job_stop = $job[stop_timestamp];
+			$runningtime = intval( $job_stop - $job_start );
+			$tpl->assign( "started", makeDate( $job_start ) );
+			$tpl->assign( "finished", makeDate( $job_stop ) );
+			$tpl->assign( "runningtime", makeTime( $runningtime ) );
+			
+			//print_r( $job );
+			//print_r( $nodes );
+		}
+
+		if( count( $search_ids ) == 1 ) {
+
+			$tpl->newBlock( "showhosts" );
+
+			$showhosts = isset($sh) ? $sh : $default_showhosts;
+			//if( !$showhosts) $showhosts = $default_showhosts;
+			$tpl->assign("checked$showhosts", "checked");
+
+			# Present a width list
+			$cols_menu = "<SELECT NAME=\"hc\" OnChange=\"archive_search_form.submit();\">\n";
+
+			$hostcols = ($hc) ? $hc : 4;
+
+			foreach(range(1,25) as $cols) {
+				$cols_menu .= "<OPTION VALUE=$cols ";
+				if ($cols == $hostcols)
+					$cols_menu .= "SELECTED";
+				$cols_menu .= ">$cols\n";
+			}
+			$cols_menu .= "</SELECT>\n";
+
+			$tpl->assign("metric","$metricname $units");
+			$tpl->assign("id", $id);
+			# Host columns menu defined in header.php
+			$tpl->assign("cols_menu", $cols_menu);
+
+			if( $showhosts ) {
+				//bla
+
+				//printf("job_start = %s job_stop = %s\n", $job_start, $job_stop );
+				//printf("start = %s stop = %s\n", $start, $stop );
+
+				if( !$period_start ) // Add an extra 10% to graphstart
+					$period_start = intval( $job_start - (intval( $runningtime * 0.10 ) ) );
+				else
+					$period_start = datetimeToEpoch( $period_start );
+
+				if( !$period_stop ) // Add an extra 10% to graphend
+					$period_stop = intval( $job_stop + (intval( $runningtime * 0.10 ) ) );
+				else
+					$period_stop = datetimeToEpoch( $period_stop );
+
+				//printf("start = %s stop = %s\n", $start, $stop );
+
+		                $tpl->gotoBlock( "timeperiod" );
+
+				$tpl->assign("period_start", epochToDatetime( $period_start ) );
+				$tpl->assign("period_stop", epochToDatetime( $period_stop ) );
+
+		                $tpl->gotoBlock( "_ROOT" );
+
+				$hosts_up = array();
+
+				foreach( $nodes[$id] as $mynode )
+					$hosts_up[] = $mynode[hostname];
+
+				$sorted_hosts = array();
+				//$hosts_up = $jobs[$filter[id]][nodes];
+
+				foreach ($hosts_up as $host ) {
+					//$host = $host. '.'.$job_domain;
+					$cpus = $metrics[$host]["cpu_num"][VAL];
+					if (!$cpus) $cpus=1;
+					$load_one  = $metrics[$host]["load_one"][VAL];
+					$load = ((float) $load_one)/$cpus;
+					$host_load[$host] = $load;
+					$percent_hosts[load_color($load)] += 1;
+					if ($metricname=="load_one")
+						$sorted_hosts[$host] = $load;
+					else
+						$sorted_hosts[$host] = $metrics[$host][$metricname][VAL];
+				}
+				switch ($sort) {
+					case "descending":
+						arsort($sorted_hosts);
+						break;
+					case "by hostname":
+						ksort($sorted_hosts);
+						break;
+					default:
+					case "ascending":
+						asort($sorted_hosts);
+						break;
+				}
+
+				//$sorted_hosts = array_merge($down_hosts, $sorted_hosts);
+
+				# First pass to find the max value in all graphs for this
+				# metric. The $start,$end variables comes from get_context.php,
+				# included in index.php.
+				list($min, $max) = find_limits($sorted_hosts, $metricname);
+
+				# Second pass to output the graphs or metrics.
+				$i = 1;
+				foreach ( $sorted_hosts as $host=>$value  ) {
+					$tpl->newBlock ("sorted_list");
+					//$host = $host. '.'.$domain;
+					$host_url = rawurlencode($host);
+					$cluster_url = rawurlencode($clustername);
+
+					$textval = "";
+					//printf("host = %s, value = %s", $host, $value);
+					//echo "$host: $value, ";
+					$val = $metrics[$host][$metricname];
+					$class = "metric";
+					$host_link="\"?c=$cluster_url&h=$host_url&job_start=$job_start&job_stop=$job_stop&period_start=$period_start&period_stop=$period_stop\"";
+
+					if ($val[TYPE]=="timestamp" or $always_timestamp[$metricname]) {
+						$textval = date("r", $val[VAL]);
+					} elseif ($val[TYPE]=="string" or $val[SLOPE]=="zero" or $always_constant[$metricname] or ($max_graphs > 0 and $i > $max_graphs )) {
+						$textval = "$val[VAL] $val[UNITS]";
+					} else {
+						$graphargs = "z=small&c=$cluster_url&m=$metricname&h=$host_url&v=$val[VAL]&x=$max&n=$min&job_start=$job_start&job_stop=$job_stop&period_start=$period_start&period_stop=$period_stop&min=$min&max=$max";
+					}
+					if ($textval) {
+						$cell="<td class=$class>".  "<b><a href=$host_link>$host</a></b><br>".  "<i>$metricname:</i> <b>$textval</b></td>";
+					} else {
+						$cell="<td><a href=$host_link>".  "<img src=\"./graph.php?$graphargs\" ".  "alt=\"$host\" border=0></a></td>";
+					}
+
+					$tpl->assign("metric_image", $cell);
+					if (! ($i++ % $hostcols) )
+						 $tpl->assign ("br", "</tr><tr>");
+				}
+
+				//einde bla
+			}
+		}
+
+	}
+}
+?>
Index: /tags/0.2/web/addons/job_monarch/styles.css
===================================================================
--- /tags/0.2/web/addons/job_monarch/styles.css	(revision 325)
+++ /tags/0.2/web/addons/job_monarch/styles.css	(revision 325)
@@ -0,0 +1,278 @@
+/* The Ganglia Web-frontend Cascading Style Sheet. */
+
+A:link
+{
+   color: rgb(32,41,204);
+   text-decoration: none;
+}
+
+A:visited
+{
+   color: rgb(51,51,204);
+   text-decoration: none;
+}
+
+A:active
+{
+   color: rgb(204,205,226);
+   text-decoration: none;
+}
+
+h1 
+{
+   font: bold 18pt/22pt helvetica, sans-serif;
+   color: rgb(12,17,142);
+   margin-bottom: 2mm;
+}
+
+h2
+{
+   font: bold 14pt/16pt helvetica;
+   color: rgb(12,17,142); 
+   margin-left: 0.1em;
+   margin-right: 0.1em;
+}
+
+h3
+{
+   font: bold 12pt/15pt helvetica, sans-serif;
+   text-align: left;
+   margin-left: 3mm;
+   color: rgb(247,142,14);
+}
+
+h4
+{
+   font: bold 12pt/14pt helvetica;
+   color: rgb(12,17,142); 
+   margin-left: 0.1em;
+   margin-right: 0.1em;
+}
+
+small
+{
+   font: 10pt/12pt arial, helvetica, sans-serif;
+}
+
+td.title
+{
+   font-size: larger;
+   font-weight: normal;
+   background: rgb(238,238,238);
+   text-align: center;
+}
+
+td.metric
+{
+   background: rgb(217,218,224);
+   padding: 2mm;
+}
+
+td.cluster
+{
+   background: rgb(238,238,238);
+   /* padding: 1mm; */
+}
+
+td.grid
+{
+   vertical-align: top;
+   background: rgb(204,204,255);
+}
+
+td.self
+{
+   vertical-align: top;
+   text-align: center;
+   background: rgb(255,153,153);
+}
+
+td.even
+{
+   background: rgb(201,205,234);
+}
+
+td.odd
+{
+   background: rgb(217,218,224);
+}
+
+td.small
+{
+   font: 10pt/12pt times, serif;
+}
+
+td.L1
+{
+   background: rgb(184,249,119);
+}
+
+td.L2
+{
+   background: rgb(213,249,119);
+}
+
+td.L3
+{
+   background: rgb(228,249,119);
+}
+
+td.L4
+{
+   background: rgb(245,249,119);
+}
+
+td.L5
+{
+   background: rgb(249,239,119);
+}
+
+td.L6
+{
+   background: rgb(249,219,119);
+}
+
+td.L7
+{
+   background: rgb(249,202,119);
+}
+
+td.L8
+{
+   background: rgb(249,182,119);
+}
+
+td.L9
+{
+   background: rgb(249,152,119);
+}
+
+td.L10
+{
+   background: rgb(249,124,119);
+}
+
+td.down
+{
+   background: rgb(255,153,153);
+}
+
+th 
+{
+   font: bold 12pt/14pt helvetica, sans-serif;
+   color: rgb(238,238,238);
+   background: rgb(80,80,80);
+   border: thin solid rgb(47,47,47);
+}
+
+tr.even
+{
+   background: rgb(255,255,255);
+}
+
+tr.odd
+{
+   background: rgb(240,240,240);
+}
+
+tr.inactive
+{
+   color: rgb(210,210,210);
+}
+
+table.overview
+{
+   border-collapse: collapse;
+}
+
+*#kol1
+{
+   border-right-style: solid;
+   border-right-color: rgb(204,204,204);
+}
+
+*#selected
+{
+   background: rgb(153,204,255);
+}
+
+tr.overview_header
+{
+   font: bold 10pt/12pt helvetica, sans-serif;
+   text-align: right;
+   border-collapse: collapse;
+   border-bottom-style: solid; 
+   border-bottom-color: rgb(0,0,0);
+}
+
+tr.overview_line
+{
+   font: 10pt/12pt helvetica, sans-serif;
+   text-align: right;
+   border-collapse: collapse;
+   border-bottom-style: dotted;
+   border-bottom-color: rgb(204,204,204);
+}
+
+tr.overview td
+{
+   font: 10pt/12pt helvetica, sans-serif;
+   text-align: right;
+}
+
+tr.monarch th
+{
+   font: bold 12pt/14pt helvetica, sans-serif;
+   color: rgb(10,10,10);
+   background: rgb(240,240,240);
+   border: thin solid rgb(47,47,47);
+   padding: 2mm;
+   margin: 1mm;
+}
+
+.jobname_hint
+{
+   border-bottom: thin dotted;
+   border-bottom-color: rgb(255,0,0);
+}
+
+.red
+{
+	color: rgb(204,51,0);
+}
+.blue
+{
+	color: rgb(0,0,153);
+}
+.brown
+{
+	color: rgb(102,102,0);
+}
+.gray
+{
+	color: rgb(204,204,204);
+}
+.green
+{
+	color: rgb(0,204,102);
+}
+
+td em
+{
+   font: 8pt/10pt helvetica, sans-serif;
+   color: rgb(31,31,112);
+   font-style: normal;
+}
+
+td strong
+{
+   color: rgb(31,31,112);
+   font-style: normal;
+}
+
+.footer
+{
+      font: 10pt/12pt helvetica, sans-serif;
+      color: rgb(100,100,100);
+}
+
Index: /tags/0.2/web/addons/job_monarch/templates/footer.tpl
===================================================================
--- /tags/0.2/web/addons/job_monarch/templates/footer.tpl	(revision 325)
+++ /tags/0.2/web/addons/job_monarch/templates/footer.tpl	(revision 325)
@@ -0,0 +1,16 @@
+<HR>
+<CENTER>
+<FONT SIZE="-1" class=footer>
+<A HREF="https://subtrac.sara.nl/oss/jobmonarch/">Job Monarch</A> version {monarch-version}
+<A HREF="https://subtrac.sara.nl/oss/jobmonarch/">Check for Updates.</A><BR>
+
+Ganglia version {webfrontend-version}<BR>
+
+Downloading and parsing ganglia's XML tree took {parsetime}.<BR>
+Images created with <A HREF="http://www.rrdtool.com/">RRDTool</A>.<BR>
+</FONT>
+</CENTER>
+
+</FORM>
+</BODY>
+</HTML>
Index: /tags/0.2/web/addons/job_monarch/templates/header.tpl
===================================================================
--- /tags/0.2/web/addons/job_monarch/templates/header.tpl	(revision 325)
+++ /tags/0.2/web/addons/job_monarch/templates/header.tpl	(revision 325)
@@ -0,0 +1,97 @@
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
+<HTML>
+<HEAD>
+<TITLE>Ganglia:: {page_title}</TITLE>
+<META http-equiv="Content-type" content="text/html; charset=utf-8">
+<META http-equiv="refresh" content="{refresh}{redirect}" >
+<LINK rel="stylesheet" href="./styles.css" type="text/css">
+<SCRIPT LANGUAGE="javascript" SRC="ts_picker.js"></SCRIPT>
+<SCRIPT LANGUAGE="javascript">
+
+        function setPeriodTimestamps() {
+
+                document.{form_name}.period_start.value = document.{form_name}.period_start_pick.value;
+                document.{form_name}.period_stop.value = document.{form_name}.period_stop_pick.value;
+        }
+
+</SCRIPT>
+</HEAD>
+<BODY BGCOLOR="#FFFFFF">
+
+<FORM ACTION="./" METHOD="GET" NAME="{form_name}">
+<INPUT TYPE="HIDDEN" NAME="c" VALUE="{cluster}">
+<INPUT TYPE="HIDDEN" NAME="view" VALUE="{view}">
+<TABLE WIDTH="100%">
+<TR>
+  <TD ROWSPAN="2" WIDTH="150">
+  <A HREF="http://monitor2.irc.sara.nl/ganglia/">
+  <IMG SRC="./logo_ned.gif" 
+      ALT="Ganglia" BORDER="0"></A>
+  </TD>
+  <TD VALIGN="TOP">
+
+  <TABLE WIDTH="100%" CELLPADDING="8" CELLSPACING="0" BORDER=0>
+  <TR BGCOLOR="#DDDDDD">
+     <TD BGCOLOR="#DDDDDD">
+     <FONT SIZE="+1">
+     <B>{page_title} for {date}</B>
+     </FONT>
+     </TD>
+     <TD BGCOLOR="#DDDDDD" ALIGN="RIGHT">
+     <INPUT TYPE="SUBMIT" VALUE="Get Fresh Data">
+     </TD>
+     <TD></TD>
+  </TR>
+  <TR>
+     <TD COLSPAN=1>
+     {metric_menu} &nbsp;&nbsp;
+     {range_menu}&nbsp;&nbsp;
+     {sort_menu}&nbsp;&nbsp;
+<!-- START BLOCK : timeperiod -->
+    <INPUT TYPE="HIDDEN" NAME="period_start" VALUE="{period_start}">
+    <INPUT TYPE="HIDDEN" NAME="period_stop" VALUE="{period_stop}">
+    <INPUT TYPE="HIDDEN" NAME="h" VALUE="{hostname}">
+    <BR><BR><B>Graph/ from
+    <INPUT TYPE="text" NAME="period_start_pick" VALUE="{period_start}" ALT="Start time" DISABLED="TRUE">
+    <a href="javascript:show_calendar('document.{form_name}.period_start_pick', document.{form_name}.period_start_pick.value);" alt="Click to select a date/time" title="Click to select a date/time">
+    <img src="cal.gif" width="16" height="16" border="0"></a>
+    <a href="#" onClick="javascript: document.{form_name}.period_start_pick.value=''" alt="Click here to clear field" title="Click here to clear field">
+    <IMG SRC="redcross.jpg" BORDER=0></A>
+    to <INPUT TYPE="text" NAME="period_stop_pick" VALUE="{period_stop}" ALT="Stop time" DISABLED="TRUE">
+    <a href="javascript:show_calendar('document.{form_name}.period_stop_pick', document.{form_name}.period_stop_pick.value);" alt="Click to select a date/time" title="Click to select a date/time">
+    <img src="cal.gif" width="16" height="16" border="0"></a>
+    <a href="#" onClick="javascript: document.{form_name}.period_stop_pick.value=''" alt="Click here to clear field" title="Click here to clear field">
+    </B>
+    <IMG SRC="redcross.jpg" BORDER=0></A>
+
+<!-- START BLOCK : hostview -->
+    <INPUT TYPE="HIDDEN" NAME="job_start" VALUE="{job_start}">
+    <INPUT TYPE="HIDDEN" NAME="job_stop" VALUE="{job_stop}">
+<!-- END BLOCK : hostview -->
+    <INPUT TYPE="submit" onClick="setPeriodTimestamps();" VALUE="Refresh graphs">
+<!-- END BLOCK : timeperiod -->
+     </TD>
+     <TD>
+      <B>{alt_view}</B>
+     </TD>
+
+  </TR>
+  </TABLE>
+
+<!-- START BLOCK : search -->
+     <TD align="right"><CENTER>
+       <A HREF="./?c={cluster_url}&view=search">
+       <B><I>Jobarchive</I></B><BR>
+       <IMG SRC="./document_archive.jpg" HEIGHT=100 ALT="Search the archive for {cluster}" TITLE="Search the archive for {cluster}" BORDER=0></A></CENTER>
+     </TD>
+<!-- END BLOCK : search -->
+
+  </TD>
+</TR>
+</TABLE> 
+
+<FONT SIZE="+1">
+{node_menu}
+</FONT>
+
+<HR SIZE="1" NOSHADE>
Index: /tags/0.2/web/addons/job_monarch/templates/host_view.tpl
===================================================================
--- /tags/0.2/web/addons/job_monarch/templates/host_view.tpl	(revision 325)
+++ /tags/0.2/web/addons/job_monarch/templates/host_view.tpl	(revision 325)
@@ -0,0 +1,91 @@
+<TABLE BORDER="0" WIDTH="100%">
+<TR>
+  <TD COLSPAN="2" BGCOLOR="#EEEEEE" ALIGN="CENTER">
+  <FONT SIZE="+2">{host} Overview</FONT>
+  </TD>
+</TR>
+
+<TR>
+ <TD ALIGN="LEFT" VALIGN="TOP">
+
+<IMG SRC="{node_image}" HEIGHT="60" WIDTH="30" ALT="{host}" BORDER="0">
+{node_msg}
+<P>
+
+<TABLE BORDER="0" WIDTH="100%">
+<TR>
+  <TD COLSPAN="2" CLASS=title>Time and String Metrics</TD>
+</TR>
+
+<!-- START BLOCK : string_metric_info -->
+<TR>
+ <TD CLASS=footer WIDTH=30%>{name}</TD><TD>{value}</TD>
+</TR>
+<!-- END BLOCK : string_metric_info -->
+
+<TR><TD>&nbsp;</TD></TR>
+
+<TR>
+  <TD COLSPAN=2 CLASS=title>Constant Metrics</TD>
+</TR>
+
+<!-- START BLOCK : const_metric_info -->
+<TR>
+ <TD CLASS=footer WIDTH=30%>{name}</TD><TD>{value}</TD>
+</TR>
+<!-- END BLOCK : const_metric_info -->
+
+<TR><TD>&nbsp;</TD></TR>
+
+<TR>
+ <TD COLSPAN=2 CLASS=title>
+ <a href="../../host_gmetrics.php?c={cluster_url}&h={host}">Gmetrics</a>
+ </TD>
+</TR>
+</TABLE>
+
+ <HR>
+<!-- INCLUDE BLOCK : extra -->
+
+</TD>
+
+<TD ALIGN="CENTER" VALIGN="TOP" WIDTH="395">
+<IMG HEIGHT="147" WIDTH="395" ALT="{cluster_url} LOAD"
+   SRC="./graph.php?g=load_report&z=medium&c={cluster_url}&{graphargs}">
+<IMG HEIGHT="160" WIDTH="395" ALT="{cluster_url} MEM"
+   SRC="./graph.php?g=mem_report&z=medium&c={cluster_url}&{graphargs}">
+<IMG HEIGHT="147" WIDTH="395" ALT="{cluster_url} CPU"
+   SRC="./graph.php?g=cpu_report&z=medium&c={cluster_url}&{graphargs}">
+<IMG HEIGHT="147" WIDTH="395" ALT="{cluster_url} NETWORK"
+   SRC="./graph.php?g=network_report&z=medium&c={cluster_url}&{graphargs}">
+<IMG HEIGHT="147" WIDTH="395" ALT="{cluster_url} PACKETS"
+   SRC="./graph.php?g=packet_report&z=medium&c={cluster_url}&{graphargs}">
+
+</TD>
+</TR>
+</TABLE>
+<BR>
+<BR>
+<TABLE BORDER="0" WIDTH="100%">
+<TR>
+  <TD CLASS=title>
+  {host} <strong>graphs</strong>
+  last <strong>{range}</strong>
+  sorted <strong>{sort}</strong>
+  </TD>
+</TR>
+</TABLE>
+
+<CENTER>
+<TABLE>
+<TR>
+ <TD>
+
+<!-- START BLOCK : vol_metric_info -->
+<IMG HEIGHT="147" WIDTH="395" ALT="{alt}" SRC="./graph.php?{graphargs}">{br}
+<!-- END BLOCK : vol_metric_info -->
+
+ </TD>
+</TR>
+</TABLE>
+</CENTER>
Index: /tags/0.2/web/addons/job_monarch/templates/index.tpl
===================================================================
--- /tags/0.2/web/addons/job_monarch/templates/index.tpl	(revision 325)
+++ /tags/0.2/web/addons/job_monarch/templates/index.tpl	(revision 325)
@@ -0,0 +1,9 @@
+<!-- TOGA SOB: <header> -->
+<!-- INCLUDE BLOCK : header -->
+<!-- TOGA EOB: </header> -->
+<!-- TOGA SOB: <main> -->
+<!-- INCLUDE BLOCK : main -->
+<!-- TOGA EOB: </main> -->
+<!-- TOGA SOB: <footer> -->
+<!-- INCLUDE BLOCK : footer -->
+<!-- TOGA EOB: </footer> -->
Index: /tags/0.2/web/addons/job_monarch/templates/overview.tpl
===================================================================
--- /tags/0.2/web/addons/job_monarch/templates/overview.tpl	(revision 325)
+++ /tags/0.2/web/addons/job_monarch/templates/overview.tpl	(revision 325)
@@ -0,0 +1,228 @@
+<BR><BR>
+
+<CENTER>
+<TABLE cellpadding="15">
+<TR>
+
+  <TD>
+
+<TABLE ALIGN=CENTER class="overview" cellpadding="5">
+
+<COL id="kol1">
+<COL id="kol2">
+<COL id="kol3">
+<COL id="kol4">
+
+<TR class="overview_header">
+<TD>Batch</TD>
+<TD>Jobs</TD>
+<TD>Nodes</TD>
+<TD>Cpus</TD>
+</TR>
+
+<TR class="overview_line">
+<TD class="blue">
+Capacity
+</TD>
+<TD class="blue">
+</TD>
+<TD class="blue">
+{avail_nodes}
+</TD>
+<TD class="blue">
+{avail_cpus}
+</TD>
+</TR>
+
+
+<TR class="overview">
+<TD class="red">
+Running
+</TD>
+<TD class="red">
+{running_jobs}
+</TD>
+<TD class="red">
+{running_nodes}
+</TD>
+<TD class="red">
+{running_cpus}
+</TD>
+</TR>
+
+<TR class="overview_line">
+<TD class="gray">
+Queued
+</TD>
+<TD class="gray">
+{queued_jobs}
+</TD>
+<TD class="gray">
+{queued_nodes}
+</TD>
+<TD class="gray">
+{queued_cpus}
+</TD>
+</TR>
+
+<TR class="overview">
+<TD class="brown">
+Total
+</TD>
+<TD class="brown">
+{total_jobs}
+</TD>
+<TD class="brown">
+{total_nodes}
+</TD>
+<TD class="brown">
+{total_cpus}
+</TD>
+</TR>
+
+<TR class="overview">
+<TD class="green">
+Free
+</TD>
+<TD class="green">
+</TD>
+<TD class="green">
+{free_nodes}
+</TD>
+<TD class="green">
+{free_cpus}
+</TD>
+</TR>
+
+<TR class="overview" id="selected">
+<TD>
+View
+</TD>
+<TD>
+{view_jobs}
+</TD>
+<TD>
+{view_nodes}
+</TD>
+<TD>
+{view_cpus}
+</TD>
+</TR>
+
+</TABLE>
+
+  <TD ALIGN="CENTER"><CENTER>
+    <IMG SRC="{clusterimage}"><BR>
+<FONT class="footer">Last updated: {report_time}</FONT></CENTER>
+  </TD>
+
+  </TD>
+  <TD ALIGN="CENTER">
+    <IMG SRC="{pie}">
+  </TD>
+</TR>
+</TABLE>
+
+<BR>
+
+<SCRIPT TYPE="text/javascript" SRC="libtoga.js"></SCRIPT>
+
+<INPUT TYPE="HIDDEN" NAME="sortby" VALUE="{sortby}">
+<INPUT TYPE="HIDDEN" NAME="sortorder" VALUE="{sortorder}">
+<INPUT TYPE="HIDDEN" NAME="c" VALUE="{clustername}">
+<INPUT TYPE="HIDDEN" NAME="queue" VALUE="{f_queue}">
+<INPUT TYPE="HIDDEN" NAME="state" VALUE="{f_state}">
+<INPUT TYPE="HIDDEN" NAME="user" VALUE="{f_user}">
+<INPUT TYPE="HIDDEN" NAME="id" VALUE="{f_id}">
+<INPUT TYPE="HIDDEN" NAME="filterorder" VALUE="{f_order}">
+
+<TABLE WIDTH="100%" CELLPADDING="2" CELLSPACING="2" BORDER=0>
+<TR CLASS="monarch">
+<TH><B><A HREF="#" onClick="setSort( 'id' )" ALT="Jobid" TITLE="Jobid">Id</A></B></TH>
+<TH><B><A HREF="#" onClick="setSort( 'state' )" ALT="State" TITLE="State">S</A></B></TH>
+<TH><B><A HREF="#" onClick="setSort( 'user' )">User</A></B></TH>
+<TH><B><A HREF="#" onClick="setSort( 'queue' )">Queue</A></B></TH>
+<TH><B><A HREF="#" onClick="setSort( 'name' )" ALT="Jobname" TITLE="Jobname">Name</A></B></TH>
+<TH><B><A HREF="#" onClick="setSort( 'req_cpu' )" ALT="Requested CPU Time (walltime)" TITLE="Requested CPU Time (walltime)">Req. CPU time</A></B></TH>
+<!-- START BLOCK : column_header_req_mem -->
+<TH><B><A HREF="#" onClick="setSort( 'req_mem' )" ALT="Requested Memory" TITLE="Requested Memory">Req. Memory</A></B></TH>
+<!-- END BLOCK : column_header_req_mem -->
+<TH><B><A HREF="#" onClick="setSort( 'nodes' )" ALT="Nodes" TITLE="Nodes">N</A>/<A HREF="#" onClick="setSort( 'cpus' )" ALT="Processors" TITLE="Processors">P</A></B></TH>
+<!-- START BLOCK : column_header_queued -->
+<TH><B><A HREF="#" onClick="setSort( 'queued' )">Queued</A></B></TH>
+<!-- END BLOCK : column_header_queued -->
+<!-- START BLOCK : column_header_nodes -->
+<TH><B><A HREF="#" onClick="setSort( 'nodes' )" ALT="Nodes" TITLE="Nodes">Nodes</A></B></TH>
+<!-- END BLOCK : column_header_nodes -->
+<TH><B><A HREF="#" onClick="setSort( 'start' )">Started</A></B></TH>
+<TH><B><A HREF="#" onClick="setSort( 'runningtime' )">Runningtime</A></B></TH>
+</TR>
+
+<!-- START BLOCK : node -->
+  <TR CLASS="{nodeclass}">
+    <TD><A HREF="#" onClick="setFilter( 'id', '{id}' )">{id}</A></TD>
+    <TD><A HREF="#" onClick="setFilter( 'state', '{state}' )" ALT="{fullstate}" TITLE="{fullstate}">{state}</A></TD>
+    <TD><A HREF="#" onClick="setFilter( 'user', '{user}' )">{user}</A></TD>
+    <TD><A HREF="#" onClick="setFilter( 'queue', '{queue}' )">{queue}</A></TD>
+    <TD ALT="{fulljobname}" TITLE="{fulljobname}">
+<!-- START BLOCK : jobname_hint_start -->
+    <FONT CLASS="jobname_hint">
+<!-- END BLOCK : jobname_hint_start -->
+    {name}
+<!-- START BLOCK : jobname_hint_end -->
+    </FONT>
+<!-- END BLOCK : jobname_hint_end -->
+    </TD>
+    <TD>{req_cpu}</TD>
+<!-- START BLOCK : column_req_mem -->
+    <TD>{req_memory}</TD>
+<!-- END BLOCK : column_req_mem -->
+    <TD>{nodes}/{cpus}</TD>
+<!-- START BLOCK : column_queued -->
+    <TD>{queued}</TD>
+<!-- END BLOCK : column_queued -->
+<!-- START BLOCK : column_nodes -->
+    <TD>{nodes_hostnames}</TD>
+<!-- END BLOCK : column_nodes -->
+    <TD>{started}</TD>
+    <TD>{runningtime}</TD>
+  </TR>
+<!-- END BLOCK : node -->
+</TABLE>
+</CENTER>
+
+<!-- START BLOCK : showhosts -->
+<TABLE BORDER="0" WIDTH="100%">
+<TR>
+  <TD CLASS=title COLSPAN="2">
+  <FONT SIZE="-1">
+  Show Hosts:
+  yes<INPUT type=radio name="sh" value="1" OnClick="toga_form.submit();" {checked1}>
+  no<INPUT type=radio name="sh" value="0" OnClick="toga_form.submit();" {checked0}>
+  </FONT>
+  |
+  job <strong>{id}</strong> metric <strong>{metric}</strong>
+  |
+   <FONT SIZE="-1">
+   Columns&nbsp;&nbsp;{cols_menu}
+   </FONT>
+  </TD>
+</TR>
+   
+</TABLE>
+
+<CENTER>
+<TABLE>
+<TR>
+<!-- START BLOCK : sorted_list -->
+{metric_image}{br}
+<!-- END BLOCK : sorted_list -->
+</TR>
+</TABLE>
+
+<p>
+(Nodes colored by 1-minute load) | <A HREF="../../node_legend.html" ALT="Node Image egend">Legend</A>
+
+</CENTER>
+
+<!-- END BLOCK : showhosts -->
Index: /tags/0.2/web/addons/job_monarch/templates/search.tpl
===================================================================
--- /tags/0.2/web/addons/job_monarch/templates/search.tpl	(revision 325)
+++ /tags/0.2/web/addons/job_monarch/templates/search.tpl	(revision 325)
@@ -0,0 +1,288 @@
+<CENTER>
+
+<SCRIPT LANGUAGE="javascript">
+function setSort( sortbyval ) {
+
+        if( sortbyval != document.archive_search_form.sortby.value ) {
+
+                document.archive_search_form.sortby.value = sortbyval;
+                document.archive_search_form.sortorder.value = "asc";
+
+        } else {
+
+                if( document.archive_search_form.sortorder.value == "desc" )
+                        document.archive_search_form.sortorder.value = "asc";
+                else if( document.archive_search_form.sortorder.value == "asc" )
+                        document.archive_search_form.sortorder.value = "desc";
+        }
+
+        document.forms['archive_search_form'].submit();
+}
+
+function setFilter( filtername, filterval ) {
+
+        var myfilterorder = document.archive_search_form.elements['filterorder'].value;
+
+        if( document.archive_search_form.elements[filtername] ) {
+                document.archive_search_form.elements[filtername].value = filterval;
+                if( myfilterorder != '')
+                        myfilterorder = myfilterorder + "," + filtername;
+                else
+                        myfilterorder = filtername;
+
+        }
+        document.archive_search_form.elements['filterorder'].value = myfilterorder;
+
+        //setTimeout( "document.forms['archive_search_form'].submit();", 1000 );
+
+        document.forms['archive_search_form'].submit();
+}
+</SCRIPT>
+<SCRIPT LANGUAGE="javascript">
+
+	function setSearchTimestamps() {
+
+		document.archive_search_form.start_from_time.value = document.archive_search_form.start_from_pick.value; 
+		document.archive_search_form.start_to_time.value = document.archive_search_form.start_to_pick.value;
+		document.archive_search_form.end_from_time.value = document.archive_search_form.end_from_pick.value; 
+		document.archive_search_form.end_to_time.value = document.archive_search_form.end_to_pick.value;
+	}
+
+</SCRIPT>
+
+<!-- <FORM NAME="archive_search_form" ACTION="./"> -->
+
+<!-- <INPUT TYPE="hidden" NAME="view" VALUE="search"> -->
+
+<BR><BR>
+
+<TABLE WIDTH="100%">
+
+<TR>
+  <TD CLASS=title COLSPAN="2">
+  <B>Search job archive</B>
+  </TD>
+</TR>
+
+</TABLE>
+
+<BR><BR>
+
+<TABLE WIDTH="90%">
+
+<TR>
+  <TD></TD>
+  <TD></TD>
+  <TD></TD>
+  <TD></TD>
+  <TD></TD>
+  <TD></TD>
+</TR>
+
+<TR>
+
+  <TD>
+  Id:
+  </TD>
+  <TD>
+  <INPUT TYPE="TEXT" NAME="id" VALUE="{id_value}">
+  </TD>
+
+</TR>
+
+<TR>
+
+  <TD>
+  User:
+  </TD>
+  <TD>
+  <INPUT TYPE="TEXT" NAME="user" VALUE="{user_value}">
+  </TD>
+
+  <TD>
+  Queue:
+  </TD>
+  <TD>
+  <INPUT TYPE="TEXT" NAME="queue" VALUE="{queue_value}">
+  </TD>
+
+  <TD>
+  Name:
+  </TD>
+  <TD>
+  <INPUT TYPE="TEXT" NAME="name" VALUE="{name_value}">
+  </TD>
+
+</TR>
+
+<TR>
+
+  <TD></TD>
+  <TD>
+  Job <B>start</B>
+  </TD>
+
+  <TD>
+  From:
+  </TD>
+  <TD>
+  <INPUT TYPE="HIDDEN" NAME="start_from_time" VALUE="{start_from_value}">
+  <INPUT TYPE="TEXT" NAME="start_from_pick" VALUE="{start_from_value}" DISABLED="TRUE"> 
+  <A HREF="javascript:show_calendar('document.archive_search_form.start_from_pick', document.archive_search_form.start_from_pick.value );">
+  <IMG SRC="cal.gif" width="16" height="16" border="0" title="Click to select a date/time" alt="Click to select a date/time"></a> 
+  <a href="#" onClick="document.archive_search_form.start_from_pick.value=''" alt="Click here to clear field" title="Click here to clear field"><IMG SRC="redcross.jpg" BORDER=0></A>
+  </TD>
+  
+  <TD>
+  To:
+  </TD>
+  <TD>
+  <INPUT TYPE="HIDDEN" NAME="start_to_time" VALUE="{start_to_value}">
+  <INPUT TYPE="TEXT" NAME="start_to_pick" VALUE="{start_to_value}" DISABLED="TRUE">
+  <a href="javascript:show_calendar('document.archive_search_form.start_to_pick', document.archive_search_form.start_to_pick.value );"> 
+  <img src="cal.gif" width="16" height="16" border="0" title="Click to select a date/time" alt="Click to select a date/time"></a> 
+  <a href="#" onClick="document.archive_search_form.start_to_pick.value=''" alt="Click here to clear field" title="Click here to clear field">
+  <IMG SRC="redcross.jpg" BORDER=0></A>
+  </TD>
+  
+</TR>
+
+<TR>
+
+  <TD></TD>
+  <TD>
+  Job <B>finish</B>
+  </TD>
+
+  <TD>
+  From:
+  </TD>
+  <TD>
+  <INPUT TYPE="HIDDEN" NAME="end_from_time" VALUE="{end_from_value}">
+  <INPUT TYPE="TEXT" NAME="end_from_pick" VALUE="{end_from_value}" DISABLED="TRUE"> 
+  <A HREF="javascript:show_calendar('document.archive_search_form.end_from_pick', document.archive_search_form.end_from_pick.value );">
+  <IMG SRC="cal.gif" width="16" height="16" border="0" title="Click to select a date/time" alt="Click to select a date/time"></a> 
+  <a href="#" onClick="document.archive_search_form.end_from_pick.value=''" alt="Click here to clear field" title="Click here to clear field">
+  <IMG SRC="redcross.jpg" BORDER=0></A>
+  </TD>
+  
+  <TD>
+  To:
+  </TD>
+  <TD>
+  <INPUT TYPE="HIDDEN" NAME="end_to_time" VALUE="{end_to_value}">
+  <INPUT TYPE="TEXT" NAME="end_to_pick" VALUE="{end_to_value}" DISABLED="TRUE">
+  <a href="javascript:show_calendar('document.archive_search_form.end_to_pick', document.archive_search_form.end_to_pick.value );"> 
+  <img src="cal.gif" width="16" height="16" border="0" title="Click to select a date/time" alt="Click to select a date/time"></a> 
+  <a href="#" onClick="document.archive_search_form.end_to_pick.value=''" alt="Click here to clear field" title="Click here to clear field">
+  <IMG SRC="redcross.jpg" BORDER=0></A>
+  </TD>
+  
+</TR>
+
+<TR>
+
+  <TD></TD>
+  <TD></TD>
+  <TD></TD>
+  <TD></TD>
+  <TD></TD>
+  <TD>
+  <INPUT TYPE="submit" VALUE="Search archive" onClick="setSearchTimestamps();">
+  </TD>
+
+</TR>
+
+</TABLE>
+{form_error_msg}<BR><BR>
+<!-- START BLOCK : search_results -->
+
+<INPUT TYPE="HIDDEN" NAME="sortby" VALUE="{sortby}">
+<INPUT TYPE="HIDDEN" NAME="sortorder" VALUE="{sortorder}">
+<INPUT TYPE="HIDDEN" NAME="filterorder" VALUE="{f_order}">
+
+<TABLE WIDTH="100%" CELLPADDING="2" CELLSPACING="2" BORDER=0>
+<TR CLASS="monarch">
+<TH><B><A HREF="#" onClick="setSort( 'id' )" ALT="Jobid" TITLE="Jobid">Id</A></B></TH>
+<TH><B><A HREF="#" onClick="setSort( 'state' )" ALT="State" TITLE="State">S</A></B></TH>
+<TH><B><A HREF="#" onClick="setSort( 'user' )">User</A></B></TH>
+<TH><B><A HREF="#" onClick="setSort( 'queue' )">Queue</A></B></TH>
+<TH><B><A HREF="#" onClick="setSort( 'name' )" ALT="Jobname" TITLE="Jobname">Name</A></B></TH>
+<TH><B><A HREF="#" onClick="setSort( 'req_cpu' )" ALT="Requested CPU Time (walltime)" TITLE="Requested CPU Time (walltime)">Req. CPU time</A></B></TH>
+<!-- START BLOCK : column_header_req_mem -->
+<TH><B><A HREF="#" onClick="setSort( 'req_mem' )" ALT="Requested Memory" TITLE="Requested Memory">Req. Memory</A></B></TH>
+<!-- END BLOCK : column_header_req_mem -->
+<TH><B><A HREF="#" onClick="setSort( 'nodes' )" ALT="Nodes" TITLE="Nodes">N</A>/<A HREF="#" onClick="setSort( 'cpus' )" ALT="Processors" TITLE="Processors">P</A></B></TH>
+<!-- START BLOCK : column_header_nodes -->
+<TH><B><A HREF="#" onClick="setSort( 'nodes' )" ALT="Nodes" TITLE="Nodes">Nodes</A></B></TH>
+<!-- END BLOCK : column_header_nodes -->
+<TH><B><A HREF="#" onClick="setSort( 'start' )">Started</A></B></TH>
+<TH><B><A HREF="#" onClick="setSort( 'finished' )">Finished</A></B></TH>
+<TH><B><A HREF="#" onClick="setSort( 'runningtime' )">Runningtime</A></B></TH>
+</TR>
+
+<!-- START BLOCK : node -->
+  <TR CLASS="{nodeclass}">
+    <TD><A HREF="#" onClick="setFilter( 'id', '{id}' )">{id}</A></TD>
+    <TD><A HREF="#" onClick="setFilter( 'state', '{state}' )">{state}</A></TD>
+    <TD><A HREF="#" onClick="setFilter( 'user', '{user}' )">{user}</A></TD>
+    <TD><A HREF="#" onClick="setFilter( 'queue', '{queue}' )">{queue}</A></TD>
+    <TD ALT="{fulljobname}" TITLE="{fulljobname}">
+<!-- START BLOCK : jobname_hint_start -->
+    <FONT CLASS="jobname_hint">
+<!-- END BLOCK : jobname_hint_start -->
+    {name}
+<!-- START BLOCK : jobname_hint_end -->
+    </FONT>
+<!-- END BLOCK : jobname_hint_end -->
+    </TD>
+    <TD>{req_cpu}</TD>
+<!-- START BLOCK : column_req_mem -->
+    <TD>{req_memory}</TD>
+<!-- END BLOCK : column_req_mem -->
+    <TD>{nodes}/{cpus}</TD>
+<!-- START BLOCK : column_nodes -->
+    <TD>{nodes_hostnames}</TD>
+<!-- END BLOCK : column_nodes -->
+    <TD>{started}</TD>
+    <TD>{finished}</TD>
+    <TD>{runningtime}</TD>
+  </TR>
+<!-- END BLOCK : node -->
+</TABLE>
+</CENTER>
+
+<!-- START BLOCK : showhosts -->
+<TABLE BORDER="0" WIDTH="100%">
+<TR>
+  <TD CLASS=title COLSPAN="2">
+  <FONT SIZE="-1">
+  Show Hosts:
+  yes<INPUT type=radio name="sh" value="1" OnClick="archive_search_form.submit();" {checked1}>
+  no<INPUT type=radio name="sh" value="0" OnClick="archive_search_form.submit();" {checked0}>
+  </FONT>
+  |
+  job <strong>{id}</strong> metric <strong>{metric}</strong>
+  |
+   <FONT SIZE="-1">
+   Columns&nbsp;&nbsp;{cols_menu}
+   </FONT><BR>
+  </TD>
+</TR>
+
+</TABLE>
+
+<CENTER>
+<TABLE>
+<TR>
+<!-- START BLOCK : sorted_list -->
+{metric_image}{br}
+<!-- END BLOCK : sorted_list -->
+</TR>
+</TABLE>
+
+</CENTER>
+
+<!-- END BLOCK : showhosts -->
+<!-- END BLOCK : search_results -->
+</CENTER>
Index: /tags/0.2/web/addons/job_monarch/ts_picker.js
===================================================================
--- /tags/0.2/web/addons/job_monarch/ts_picker.js	(revision 325)
+++ /tags/0.2/web/addons/job_monarch/ts_picker.js	(revision 325)
@@ -0,0 +1,149 @@
+// Title: Timestamp picker
+// Description: See the demo at url
+// URL: http://us.geocities.com/tspicker/
+// Script featured on: http://javascriptkit.com/script/script2/timestamp.shtml
+// Version: 1.0
+// Date: 12-05-2001 (mm-dd-yyyy)
+// Author: Denis Gritcyuk <denis@softcomplex.com>; <tspicker@yahoo.com>
+// Notes: Permission given to use this script in any kind of applications if
+//    header lines are left unchanged. Feel free to contact the author
+//    for feature requests and/or donations
+//
+// Additional changes for Toga by Ramon Bastiaans:
+// - Time validation
+// - Setup of str_target on load of calendar (if it's empty)
+// - Commit new time when changed (not just when date is clicked)
+
+function show_calendar(str_target, str_datetime) {
+	var arr_months = ["January", "February", "March", "April", "May", "June",
+		"July", "August", "September", "October", "November", "December"];
+	var week_days = ["Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"];
+	var n_weekstart = 1; // day week starts from (normally 0 or 1)
+
+	var dt_datetime = (str_datetime == null || str_datetime =="" ?  new Date() : str2dt(str_datetime));
+	var dt_prev_month = new Date(dt_datetime);
+	dt_prev_month.setMonth(dt_datetime.getMonth()-1);
+	var dt_next_month = new Date(dt_datetime);
+	dt_next_month.setMonth(dt_datetime.getMonth()+1);
+	var dt_firstday = new Date(dt_datetime);
+	dt_firstday.setDate(1);
+	dt_firstday.setDate(1-(7+dt_firstday.getDay()-n_weekstart)%7);
+	var dt_lastday = new Date(dt_next_month);
+	dt_lastday.setDate(0);
+	
+	// html generation (feel free to tune it for your particular application)
+	// print calendar header
+	var str_buffer = new String (
+		"<html>\n"+
+		"<head>\n"+
+		"	<title>Calendar</title>\n"+
+		"</head>\n"+
+		"<SCRIPT LANGUAGE=\"javascript\" SRC=\"ts_validatetime.js\"></SCRIPT>\n"+
+		"<body bgcolor=\"White\" onLoad=\"validateTime( '"+str_target+ "', '"+str_datetime+"' ); if( window.opener."+str_target+ ".value == '' ) window.opener."+str_target+ ".value='"+dt2dtstr( dt_datetime )+" '+document.cal.time.value;\" onUnload=\"validateTime( '"+str_target+ "', '"+str_datetime+"' ); if( window.opener."+str_target+ ".value == '' ) window.opener."+str_target+ ".value='"+dt2dtstr( dt_datetime )+"'+document.cal.time.value;\">\n"+
+		"<table class=\"clsOTable\" cellspacing=\"0\" border=\"0\" width=\"100%\">\n"+
+		"<tr><td bgcolor=\"#4682B4\">\n"+
+		"<table cellspacing=\"1\" cellpadding=\"3\" border=\"0\" width=\"100%\">\n"+
+		"<tr>\n	<td bgcolor=\"#4682B4\"><a href=\"javascript:window.opener.show_calendar('"+
+		str_target+"', '"+ dt2dtstr(dt_prev_month)+"'+document.cal.time.value);\">"+
+		"<img src=\"prev.gif\" width=\"16\" height=\"16\" border=\"0\""+
+		" alt=\"previous month\"></a></td>\n"+
+		"	<td bgcolor=\"#4682B4\" colspan=\"5\">"+
+		"<font color=\"white\" face=\"tahoma, verdana\" size=\"2\">"
+		+arr_months[dt_datetime.getMonth()]+" "+dt_datetime.getFullYear()+"</font></td>\n"+
+		"	<td bgcolor=\"#4682B4\" align=\"right\"><a href=\"javascript:window.opener.show_calendar('"
+		+str_target+"', '"+dt2dtstr(dt_next_month)+"'+document.cal.time.value);\">"+
+		"<img src=\"next.gif\" width=\"16\" height=\"16\" border=\"0\""+
+		" alt=\"next month\"></a></td>\n</tr>\n"
+	);
+
+	var dt_current_day = new Date(dt_firstday);
+	// print weekdays titles
+	str_buffer += "<tr>\n";
+	for (var n=0; n<7; n++)
+		str_buffer += "	<td bgcolor=\"#87CEFA\">"+
+		"<font color=\"white\" face=\"tahoma, verdana\" size=\"2\">"+
+		week_days[(n_weekstart+n)%7]+"</font></td>\n";
+	// print calendar table
+	str_buffer += "</tr>\n";
+	while (dt_current_day.getMonth() == dt_datetime.getMonth() ||
+		dt_current_day.getMonth() == dt_firstday.getMonth()) {
+		// print row heder
+		str_buffer += "<tr>\n";
+		for (var n_current_wday=0; n_current_wday<7; n_current_wday++) {
+				if (dt_current_day.getDate() == dt_datetime.getDate() &&
+					dt_current_day.getMonth() == dt_datetime.getMonth())
+					// print current date
+					str_buffer += "	<td bgcolor=\"#FFB6C1\" align=\"right\">";
+				else if (dt_current_day.getDay() == 0 || dt_current_day.getDay() == 6)
+					// weekend days
+					str_buffer += "	<td bgcolor=\"#DBEAF5\" align=\"right\">";
+				else
+					// print working days of current month
+					str_buffer += "	<td bgcolor=\"white\" align=\"right\">";
+
+				if (dt_current_day.getMonth() == dt_datetime.getMonth())
+					// print days of current month
+					str_buffer += "<a href=\"javascript:window.opener."+str_target+
+					".value='"+dt2dtstr(dt_current_day)+"'+document.cal.time.value; window.close();\">"+
+					"<font color=\"black\" face=\"tahoma, verdana\" size=\"2\">";
+				else 
+					// print days of other months
+					str_buffer += "<a href=\"javascript:window.opener."+str_target+
+					".value='"+dt2dtstr(dt_current_day)+"'+document.cal.time.value; window.close();\">"+
+					"<font color=\"gray\" face=\"tahoma, verdana\" size=\"2\">";
+				str_buffer += dt_current_day.getDate()+"</font></a></td>\n";
+				dt_current_day.setDate(dt_current_day.getDate()+1);
+		}
+		// print row footer
+		str_buffer += "</tr>\n";
+	}
+	// print calendar footer
+	str_buffer +=
+		"<form name=\"cal\" onsubmit=\"var mf = window.opener."+str_target+".value.split( ' ' );window.opener."+str_target+".value=mf[0]+' '+document.cal.time.value; window.close();\">\n<tr><td colspan=\"7\" bgcolor=\"#87CEFA\">"+
+		"<font color=\"White\" face=\"tahoma, verdana\" size=\"2\">"+
+		"Time: <input type=\"text\" name=\"time\" value=\""+dt2tmstr(dt_datetime)+
+		"\" size=\"8\" maxlength=\"8\" onChange=\"validateTime('"+str_target+"', '"+str_datetime+"');\"></font></td></tr>\n</form>\n" +
+		"</table>\n" +
+		"</tr>\n</td>\n</table>\n" +
+		"</body>\n" +
+		"</html>\n";
+
+	var vWinCal = window.open("", "Calendar", 
+		"width=200,height=250,status=no,resizable=yes,top=200,left=200");
+	vWinCal.opener = self;
+	var calc_doc = vWinCal.document;
+	calc_doc.write (str_buffer);
+	calc_doc.close();
+}
+// datetime parsing and formatting routimes. modify them if you wish other datetime format
+function str2dt (str_datetime) {
+	var re_date = /^(\d+)\-(\d+)\-(\d+)\s+(\d+)\:(\d+)\:(\d+)$/;
+	if (!re_date.exec(str_datetime))
+		return alert("Invalid Datetime format: "+ str_datetime);
+	return (new Date (RegExp.$3, RegExp.$2-1, RegExp.$1, RegExp.$4, RegExp.$5, RegExp.$6));
+}
+function dt2dtstr (dt_datetime) {
+	return (new String (
+			dt_datetime.getDate()+"-"+(dt_datetime.getMonth()+1)+"-"+dt_datetime.getFullYear()+" "));
+}
+function dt2tmstr (dt_datetime) {
+
+	if( dt_datetime.getHours() < 10 )
+		var hours = '0' + dt_datetime.getHours();
+	else
+		var hours = dt_datetime.getHours();
+
+	if( dt_datetime.getMinutes() < 10 )
+		var minutes = '0' + dt_datetime.getMinutes();
+	else
+		var minutes = dt_datetime.getMinutes();
+
+	if( dt_datetime.getSeconds() < 10 )
+		var seconds = '0' + dt_datetime.getSeconds();
+	else
+		var seconds = dt_datetime.getSeconds();
+		
+	return (new String (
+			hours+":"+minutes+":"+seconds));
+}
+
Index: /tags/0.2/web/addons/job_monarch/ts_validatetime.js
===================================================================
--- /tags/0.2/web/addons/job_monarch/ts_validatetime.js	(revision 325)
+++ /tags/0.2/web/addons/job_monarch/ts_validatetime.js	(revision 325)
@@ -0,0 +1,116 @@
+function validateTime( str_target, str_datetime ) {
+
+        var time = document.cal.time.value.split( ':' );
+        var invalid = 0;
+	var dt_datetime = (str_datetime == null || str_datetime =="" ?  new Date() : str2dt(str_datetime));
+
+        if( time.length <= 1 && invalid == 0 ) {
+
+                document.cal.time.value = dt2tmstr( dt_datetime );
+                alert( 'Invalid time specified (needs to be HH:MM:SS), reset to current time.' );
+                invalid = 1;
+
+        } else if( time.length == 2 ) {
+
+                if( time[1] != '' ) {
+
+                        document.cal.time.value = document.cal.time.value + ':00';
+
+                } else if( invalid == 0 ) {
+
+                        document.cal.time.value = dt2tmstr( dt_datetime );
+                        alert( 'Invalid time specified (needs to be HH:MM:SS), reset to current time.' );
+                        invalid = 1;
+                }
+
+        } else if( time.length == 3 ) {
+
+                newtime = '';
+
+                for( var i=0; i<time.length; i++) {
+
+                        time_field = time[i];
+			if( i == 0 )
+				var sep = '';
+			else
+				var sep = ':';
+				
+
+                        if( time_field.length == 0 && invalid == 0 ) {
+
+                                document.cal.time.value = dt2tmstr( dt_datetime );
+                                alert( 'Invalid time specified (needs to be HH:MM:SS), reset to current time.' );
+                                invalid = 1;
+
+                        } else if( time_field.length == 1 ) {
+
+                                newtime = newtime + sep + '0' + time_field;
+                        } else {
+                                newtime = newtime + sep + time_field;
+                        }
+
+                        for( var l=0; l<time_field.length; l++ ) {
+                                var mydigit = time_field.charAt(l)
+
+                                if(( mydigit < "0" || mydigit > "9" ) && invalid == 0 ) {
+
+                                        document.cal.time.value = dt2tmstr( dt_datetime );
+                                        alert( 'Invalid time specified (needs to be HH:MM:SS), reset to current time.' );
+                                        invalid = 1;
+
+                                }
+                        }
+                }
+
+                if( invalid == 0 ) {
+
+                        document.cal.time.value = newtime;
+                }
+
+        }
+
+        if( invalid == 0 ) {
+
+		target = str_target.split('.');
+		frm = target[1];
+		elm = target[2];
+                var mf = window.opener.document.forms[frm].elements[elm].value.split( ' ' );
+
+		if( mf[0] != '' )
+			window.opener.document.forms[frm].elements[elm].value=mf[0]+' '+document.cal.time.value;
+		else
+			window.opener.document.forms[frm].elements[elm].value=dt2dtstr( new Date() ) +document.cal.time.value;
+        }
+}
+
+// datetime parsing and formatting routimes. modify them if you wish other datetime format
+function str2dt (str_datetime) {
+        var re_date = /^(\d+)\-(\d+)\-(\d+)\s+(\d+)\:(\d+)\:(\d+)$/;
+        if (!re_date.exec(str_datetime))
+                return alert("Invalid Datetime format: "+ str_datetime);
+        return (new Date (RegExp.$3, RegExp.$2-1, RegExp.$1, RegExp.$4, RegExp.$5, RegExp.$6));
+}
+function dt2dtstr (dt_datetime) {
+        return (new String (
+                        dt_datetime.getDate()+"-"+(dt_datetime.getMonth()+1)+"-"+dt_datetime.getFullYear()+" "));
+}
+function dt2tmstr (dt_datetime) {
+
+        if( dt_datetime.getHours() < 10 )
+                var hours = '0' + dt_datetime.getHours();
+        else
+                var hours = dt_datetime.getHours();
+
+        if( dt_datetime.getMinutes() < 10 )
+                var minutes = '0' + dt_datetime.getMinutes();
+        else
+                var minutes = dt_datetime.getMinutes();
+
+        if( dt_datetime.getSeconds() < 10 )
+                var seconds = '0' + dt_datetime.getSeconds();
+        else
+                var seconds = dt_datetime.getSeconds();
+
+        return (new String (
+                        hours+":"+minutes+":"+seconds));
+}
Index: /tags/0.2/web/addons/job_monarch/version.php
===================================================================
--- /tags/0.2/web/addons/job_monarch/version.php	(revision 325)
+++ /tags/0.2/web/addons/job_monarch/version.php	(revision 325)
@@ -0,0 +1,25 @@
+<?php
+/*
+ *
+ * This file is part of Jobmonarch
+ *
+ * Copyright (C) 2006  Ramon Bastiaans
+ *
+ * Jobmonarch 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 2 of the License, or
+ * (at your option) any later version.
+ *
+ * Jobmonarch 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, write to the Free Software
+ * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
+ *
+ */
+
+$monarchversion = "0.2";
+?>
Index: /tags/0.2/web/templates/job_monarch/cluster_extra.tpl
===================================================================
--- /tags/0.2/web/templates/job_monarch/cluster_extra.tpl	(revision 325)
+++ /tags/0.2/web/templates/job_monarch/cluster_extra.tpl	(revision 325)
@@ -0,0 +1,4 @@
+<HR>
+<A HREF="./addons/job_monarch/?c={cluster}">
+<IMG SRC="./addons/job_monarch/image.php?c={cluster}&view=small-clusterimage" BORDER=0>
+</A>
Index: /tags/0.2/web/templates/job_monarch/host_extra.tpl
===================================================================
--- /tags/0.2/web/templates/job_monarch/host_extra.tpl	(revision 325)
+++ /tags/0.2/web/templates/job_monarch/host_extra.tpl	(revision 325)
@@ -0,0 +1,4 @@
+<a href="./addons/job_monarch/?c={cluster}&h={host}">
+<IMG SRC="./addons/job_monarch/image.php?c={cluster}&h={host}&view=hostimage" BORDER=0>
+</a>
+<hr>
Index: /tags/0.2/web/templates/job_monarch/images/logo.jpg
===================================================================
--- /tags/0.2/web/templates/job_monarch/images/logo.jpg	(revision 325)
+++ /tags/0.2/web/templates/job_monarch/images/logo.jpg	(revision 325)
@@ -0,0 +1,1 @@
+link /var/www/test-ganglia/templates/default/images/logo.jpg
