Starting off the certmaster tree with most of the func code, shortly non-certmaster...
authorMichael DeHaan <mdehaan@redhat.com>
Thu, 7 Feb 2008 17:08:55 +0000 (12:08 -0500)
committerMichael DeHaan <mdehaan@redhat.com>
Thu, 7 Feb 2008 17:08:55 +0000 (12:08 -0500)
110 files changed:
LICENSE [new file with mode: 0644]
MANIFEST [new file with mode: 0644]
MANIFEST.in [new file with mode: 0644]
README [new file with mode: 0644]
certs/master-keys.py [new file with mode: 0644]
certs/slave-keys.py [new file with mode: 0644]
docs/.gitignore [new file with mode: 0644]
docs/Makefile [new file with mode: 0755]
docs/certmaster-ca.pod [new file with mode: 0644]
docs/certmaster.pod [new file with mode: 0644]
docs/func-inventory.pod [new file with mode: 0644]
docs/func.pod [new file with mode: 0644]
docs/funcd.pod [new file with mode: 0644]
etc/certmaster.conf [new file with mode: 0644]
etc/func_rotate [new file with mode: 0644]
etc/minion.conf [new file with mode: 0644]
etc/sample.acl [new file with mode: 0644]
func/CommonErrors.py [new file with mode: 0644]
func/Makefile [new file with mode: 0755]
func/SSLCommon.py [new file with mode: 0644]
func/SSLConnection.py [new file with mode: 0644]
func/__init__.py [new file with mode: 0644]
func/certmaster.py [new file with mode: 0755]
func/certs.py [new file with mode: 0644]
func/codes.py [new file with mode: 0755]
func/commonconfig.py [new file with mode: 0644]
func/config.py [new file with mode: 0644]
func/forkbomb.py [new file with mode: 0644]
func/jobthing.py [new file with mode: 0644]
func/logger.py [new file with mode: 0755]
func/minion/AuthedXMLRPCServer.py [new file with mode: 0644]
func/minion/Makefile [new file with mode: 0755]
func/minion/__init__.py [new file with mode: 0644]
func/minion/codes.py [new file with mode: 0755]
func/minion/module_loader.py [new file with mode: 0755]
func/minion/modules/Makefile [new file with mode: 0755]
func/minion/modules/__init__.py [new file with mode: 0644]
func/minion/modules/certmaster.py [new file with mode: 0644]
func/minion/modules/command.py [new file with mode: 0644]
func/minion/modules/copyfile.py [new file with mode: 0644]
func/minion/modules/filetracker.py [new file with mode: 0644]
func/minion/modules/func_module.py [new file with mode: 0644]
func/minion/modules/func_module.py.orig [new file with mode: 0644]
func/minion/modules/hardware.py [new file with mode: 0644]
func/minion/modules/jobs.py [new file with mode: 0644]
func/minion/modules/mount.py [new file with mode: 0644]
func/minion/modules/nagios-check.py [new file with mode: 0644]
func/minion/modules/netapp/README [new file with mode: 0644]
func/minion/modules/netapp/TODO [new file with mode: 0644]
func/minion/modules/netapp/__init__.py [new file with mode: 0644]
func/minion/modules/netapp/common.py [new file with mode: 0644]
func/minion/modules/netapp/snap.py [new file with mode: 0644]
func/minion/modules/netapp/vol/__init__.py [new file with mode: 0644]
func/minion/modules/netapp/vol/clone.py [new file with mode: 0644]
func/minion/modules/networktest.py [new file with mode: 0644]
func/minion/modules/process.py [new file with mode: 0644]
func/minion/modules/process.py.orig [new file with mode: 0644]
func/minion/modules/reboot.py [new file with mode: 0644]
func/minion/modules/rpms.py [new file with mode: 0644]
func/minion/modules/service.py [new file with mode: 0644]
func/minion/modules/smart.py [new file with mode: 0644]
func/minion/modules/snmp.py [new file with mode: 0644]
func/minion/modules/sysctl.py [new file with mode: 0644]
func/minion/modules/test.py [new file with mode: 0644]
func/minion/modules/virt.py [new file with mode: 0644]
func/minion/modules/yumcmd.py [new file with mode: 0644]
func/minion/server.py [new file with mode: 0755]
func/minion/sub_process.py [new file with mode: 0644]
func/minion/utils.py [new file with mode: 0755]
func/overlord/.forkbomb.py.swp [new file with mode: 0644]
func/overlord/Makefile [new file with mode: 0755]
func/overlord/__init__.py [new file with mode: 0644]
func/overlord/__init__.pyc [new file with mode: 0644]
func/overlord/client.py [new file with mode: 0755]
func/overlord/client.pyc [new file with mode: 0644]
func/overlord/cmd_modules/__init__.py [new file with mode: 0644]
func/overlord/cmd_modules/__init__.pyc [new file with mode: 0644]
func/overlord/cmd_modules/call.py [new file with mode: 0644]
func/overlord/cmd_modules/call.pyc [new file with mode: 0644]
func/overlord/cmd_modules/copyfile.py [new file with mode: 0644]
func/overlord/cmd_modules/listminions.py [new file with mode: 0644]
func/overlord/cmd_modules/ping.py [new file with mode: 0644]
func/overlord/cmd_modules/show.py [new file with mode: 0644]
func/overlord/command.py [new file with mode: 0644]
func/overlord/command.pyc [new file with mode: 0644]
func/overlord/forkbomb.pyc [new file with mode: 0644]
func/overlord/func_command.py [new file with mode: 0644]
func/overlord/func_command.pyc [new file with mode: 0644]
func/overlord/groups.py [new file with mode: 0644]
func/overlord/groups.pyc [new file with mode: 0644]
func/overlord/highlevel.py [new file with mode: 0644]
func/overlord/inventory.py [new file with mode: 0755]
func/overlord/jobthing.pyc [new file with mode: 0644]
func/overlord/modules/netapp.py [new file with mode: 0644]
func/overlord/sslclient.py [new file with mode: 0755]
func/overlord/sslclient.pyc [new file with mode: 0644]
func/overlord/test_func.py [new file with mode: 0755]
func/utils.py [new file with mode: 0755]
init-scripts/certmaster [new file with mode: 0755]
init-scripts/funcd [new file with mode: 0755]
nothing [deleted file]
scripts/Makefile [new file with mode: 0755]
scripts/certmaster [new file with mode: 0755]
scripts/certmaster-ca [new file with mode: 0755]
scripts/func [new file with mode: 0755]
scripts/func-create-module [new file with mode: 0755]
scripts/func-inventory [new file with mode: 0755]
scripts/funcd [new file with mode: 0755]
setup.py [new file with mode: 0644]
version [new file with mode: 0644]

diff --git a/LICENSE b/LICENSE
new file mode 100644 (file)
index 0000000..08ddefd
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,340 @@
+                   GNU GENERAL PUBLIC LICENSE
+                      Version 2, June 1991
+
+ Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
+ 51 Franklin Street, 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 Lesser 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 Street, 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 Lesser General
+Public License instead of this License.
+
diff --git a/MANIFEST b/MANIFEST
new file mode 100644 (file)
index 0000000..a1ab323
--- /dev/null
+++ b/MANIFEST
@@ -0,0 +1,92 @@
+AUTHORS
+LICENSE
+README
+setup.py
+version
+docs/.gitignore
+docs/Makefile
+docs/certmaster-ca.1.gz
+docs/certmaster-ca.pod
+docs/certmaster.1.gz
+docs/certmaster.pod
+docs/func-inventory.1.gz
+docs/func-inventory.pod
+docs/func.1.gz
+docs/func.pod
+docs/funcd.1.gz
+docs/funcd.pod
+etc/certmaster.conf
+etc/func_rotate
+etc/minion.conf
+etc/sample.acl
+func/CommonErrors.py
+func/SSLCommon.py
+func/SSLConnection.py
+func/__init__.py
+func/certmaster.py
+func/certs.py
+func/codes.py
+func/commonconfig.py
+func/config.py
+func/forkbomb.py
+func/jobthing.py
+func/logger.py
+func/utils.py
+func/minion/AuthedXMLRPCServer.py
+func/minion/__init__.py
+func/minion/codes.py
+func/minion/module_loader.py
+func/minion/server.py
+func/minion/sub_process.py
+func/minion/utils.py
+func/minion/modules/__init__.py
+func/minion/modules/certmaster.py
+func/minion/modules/command.py
+func/minion/modules/copyfile.py
+func/minion/modules/filetracker.py
+func/minion/modules/func_module.py
+func/minion/modules/hardware.py
+func/minion/modules/jobs.py
+func/minion/modules/mount.py
+func/minion/modules/nagios-check.py
+func/minion/modules/networktest.py
+func/minion/modules/process.py
+func/minion/modules/reboot.py
+func/minion/modules/rpms.py
+func/minion/modules/service.py
+func/minion/modules/smart.py
+func/minion/modules/snmp.py
+func/minion/modules/sysctl.py
+func/minion/modules/test.py
+func/minion/modules/virt.py
+func/minion/modules/yumcmd.py
+func/minion/modules/netapp/__init__.py
+func/minion/modules/netapp/common.py
+func/minion/modules/netapp/snap.py
+func/minion/modules/netapp/vol/__init__.py
+func/minion/modules/netapp/vol/clone.py
+func/overlord/__init__.py
+func/overlord/client.py
+func/overlord/command.py
+func/overlord/func_command.py
+func/overlord/groups.py
+func/overlord/highlevel.py
+func/overlord/inventory.py
+func/overlord/sslclient.py
+func/overlord/test_func.py
+func/overlord/cmd_modules/__init__.py
+func/overlord/cmd_modules/call.py
+func/overlord/cmd_modules/copyfile.py
+func/overlord/cmd_modules/listminions.py
+func/overlord/cmd_modules/ping.py
+func/overlord/cmd_modules/show.py
+func/overlord/modules/netapp.py
+init-scripts/certmaster
+init-scripts/funcd
+po/messages.pot
+scripts/certmaster
+scripts/certmaster-ca
+scripts/func
+scripts/func-create-module
+scripts/func-inventory
+scripts/funcd
diff --git a/MANIFEST.in b/MANIFEST.in
new file mode 100644 (file)
index 0000000..bc0e08b
--- /dev/null
@@ -0,0 +1,10 @@
+include version
+recursive-include etc *
+recursive-include docs *
+recursive-include init-scripts *
+recursive-include po *.po
+recursive-include po *.pot
+include AUTHORS
+include LICENSE
+include README
+
diff --git a/README b/README
new file mode 100644 (file)
index 0000000..6db886c
--- /dev/null
+++ b/README
@@ -0,0 +1,6 @@
+func - Fedora unified Network Controller?
+
+https://hosted.fedoraproject.org/projects/func/
+
+Source: http://git.fedoraproject.org/hosted/func.git/
+
diff --git a/certs/master-keys.py b/certs/master-keys.py
new file mode 100644 (file)
index 0000000..2c3f6e5
--- /dev/null
@@ -0,0 +1,44 @@
+#!/usr/bin/python -tt
+# 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 Library 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+# Copyright (c) 2007 Red Hat, inc 
+#- Written by Seth Vidal skvidal @ fedoraproject.org
+
+import sys
+import os
+import os.path
+import func.certs 
+
+
+cadir = '/etc/pki/func/ca'
+ca_key_file = '%s/funcmaster.key' % cadir
+ca_cert_file = '%s/funcmaster.crt' % cadir
+
+
+def main():
+    keypair = None
+    try:
+        if not os.path.exists(cadir):
+            os.makedirs(cadir)
+        if not os.path.exists(ca_key_file):
+            func.certs.create_ca(ca_key_file=ca_key_file, ca_cert_file=ca_cert_file)
+    except:
+        return 1
+        
+    return 0
+
+
+if __name__ == "__main__":
+    sys.exit(main())
+       
diff --git a/certs/slave-keys.py b/certs/slave-keys.py
new file mode 100644 (file)
index 0000000..8ddae81
--- /dev/null
@@ -0,0 +1,92 @@
+#!/usr/bin/python -tt
+# 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 Library 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+# Copyright (c) 2007 Red Hat, inc 
+#- Written by Seth Vidal skvidal @ fedoraproject.org
+
+import sys
+import os
+import os.path
+import xmlrpclib
+import time
+
+from  exceptions import Exception
+
+import func.certs
+
+
+def submit_csr_to_master(csr_file, master_uri):
+    # get csr_file
+    # submit buffer of file content to master_uri.wait_for_cert()
+    # wait for response and return
+    fo = open(csr_file)
+    csr = fo.read()
+    s = xmlrpclib.ServerProxy(master_uri)
+    
+    return s.wait_for_cert(csr)
+    
+    
+
+def main(cert_dir, master_uri):
+    keypair = None
+    key_file = '%s/slave.pem' % cert_dir
+    csr_file = '%s/slave.csr' % cert_dir
+    cert_file = '%s/slave.cert' % cert_dir
+    ca_cert_file = '%s/ca.cert' % cert_dir
+    
+    try:
+        if not os.path.exists(cert_dir):
+            os.makedirs(cert_dir)
+        if not os.path.exists(key_file):
+            keypair = func.certs.make_keypair(dest=key_file)
+        if not os.path.exists(csr_file):
+            if not keypair:
+                keypair = func.certs.retrieve_key_from_file(key_file)
+            csr = func.certs.make_csr(keypair, dest=csr_file)
+    except Exception, e: # need a little more specificity here
+        print e
+        return 1
+    
+    result = False
+    while not result:
+        result, cert_string, ca_cert_string = submit_csr_to_master(csr_file, master_uri)
+        print 'looping'
+        time.sleep(10)    
+    
+    
+    if result:
+        cert_fo = open(cert_file, 'w')
+        cert_fo.write(cert_string)
+        cert_fo.close()
+
+        ca_cert_fo = open(ca_cert_file, 'w')
+        ca_cert_fo.write(ca_cert_string)
+        ca_cert_fo.close()
+    
+    return 0
+
+
+if __name__ == "__main__":
+    if len(sys.argv[1:]) > 0: 
+        cert_dir = sys.argv[1]
+    else:
+        cert_dir = '/etc/pki/func'
+    
+    if len(sys.argv[1:]) > 1:
+        master_uri = sys.argv[2]
+    else:
+        master_uri = 'http://localhost:51235/'
+
+    sys.exit(main(cert_dir, master_uri))
+       
diff --git a/docs/.gitignore b/docs/.gitignore
new file mode 100644 (file)
index 0000000..46952a3
--- /dev/null
@@ -0,0 +1,2 @@
+# ignore compressed man pages
+*.gz
diff --git a/docs/Makefile b/docs/Makefile
new file mode 100755 (executable)
index 0000000..ede53b5
--- /dev/null
@@ -0,0 +1,7 @@
+
+
+clean::
+       @rm -fv *.pyc *~ .*~ *.pyo 
+       @find . -name .\#\* -exec rm -fv {} \;
+       @rm -fv *.rpm
+       @rm -fv *.gz    
diff --git a/docs/certmaster-ca.pod b/docs/certmaster-ca.pod
new file mode 100644 (file)
index 0000000..fce3f73
--- /dev/null
@@ -0,0 +1,41 @@
+=head1 NAME
+
+certmaster-ca -- signs certificate requests gathered by certmaster.
+
+=head1 SYNOPSIS
+
+certmaster-ca --list 
+
+certmaster-ca --sign machine.example.org
+
+=head1 DESCRIPTION
+
+"certmaster-ca --list"
+
+The list command prints all certificates that have been requested from certmaster by a remote
+service (such as funcd) but are not yet signed.
+
+func commands can't be sent to a remote machine until the certificates have been signed.
+
+"certmaster-ca --sign [hostname]"
+
+This command is used to sign a certificate and send it back to the requester.
+
+=head1 AUTO-SIGNING
+
+The certmaster can be configured to make this command unneccessary; all incoming
+requests can be signed automatically by certmaster.
+
+To configure this, edit /etc/func/certmaster.conf.
+
+=head1 ADDITONAL RESOURCES
+
+See https://hosted.fedoraproject.org/projects/func/.  It's a Wiki.
+
+See also the manpages for "func", "func-inventory", "funcd", and "certmaster".
+
+=head1 AUTHOR
+
+Various. See https://hosted.fedoraproject.org/projects/func
+
+
diff --git a/docs/certmaster.pod b/docs/certmaster.pod
new file mode 100644 (file)
index 0000000..92f5074
--- /dev/null
@@ -0,0 +1,29 @@
+=head1 NAME
+
+certmaster -- hands out certificates to funcd and other components.
+
+=head1 SYNOPSIS
+
+certmaster (it's a daemon and takes no arguments)
+
+=head1 DESCRIPTION
+
+See https://hosted.fedoraproject.org/projects/func/
+
+Certmaster is run on the master-control machine on a network being
+controlled by func.  It hands out certificates to machines running
+funcd. 
+
+Certmaster is configured by /etc/func/certmaster.conf
+
+=head1 ADDITONAL RESOURCES
+
+See https://hosted.fedoraproject.org/projects/func/.  It's a Wiki.
+
+See also the manpages for "func", "func-inventory", "funcd", "certmaster-ca".
+
+=head1 AUTHOR
+
+Various. See https://hosted.fedoraproject.org/projects/func
+
+
diff --git a/docs/func-inventory.pod b/docs/func-inventory.pod
new file mode 100644 (file)
index 0000000..cfe362d
--- /dev/null
@@ -0,0 +1,70 @@
+=head1 NAME
+
+func-inventory -- Takes inventory of data from func minions, and stores them in git.
+
+=head1 SYNOPSIS
+
+func-inventory [--verbose] [--server-spec glob] [--methods list] [--modules list] [--tree path] [--no-git]
+
+=head1 DESCRIPTION
+
+func-inventory runs against func-minions to gather information, and stores this information on the filesystem, in a tree arranged by hostname, module name, and method name.
+
+After each update, differences are commited to version control (using git), where they can be examined with tools such as "git log" and "gitk".
+
+=head1 --verbose
+
+Provides extra output about what func-inventory is doing.
+
+=head1 --server-spec
+
+A glob, as can be given to "func", that describes what machines the inventory program should run against.  The default is "*".
+
+=head1 --modules list
+
+A comma-seperated list of modules that should be included in the inventory, for instance "hardware,packages".  
+The default is "all".  
+
+=head1 --methods list
+
+A comma-seperated list of methods that should be included in the inventory, for each module being queried.  The default
+is "info", which saves the data for any module that has an "info" method.
+
+=head1 --tree-path
+
+Selects the location where func-inventory will output data.  The default is /var/lib/func/inventory.  This directory will
+contain a tree structure based on the hostnames, modules, and methods included in the inventory.
+
+=head1 --no-git
+
+Disables git integration, meaning changes will not be tracked using version control.   This option is present
+for those that do not have the "git-core" package installed, though installing it is highly recommended to get
+the full degree of power out of func-inventory.
+
+=head1 VIEWING CHANGES
+
+Since func-inventory integrates with git, all changes to the remote systems (including additions of new systems) can
+be tracked using standard git-tools such as "git log" and "gitk", when run on the directory specified for --tree.
+
+Additional built in hooks to notify changes can be written using git's own trigger mechanism, though something
+more specific to func will likely be developed in the future -- also eliminating the need to grok git internals.
+
+=head1 ALTERNATIVE OUTPUT FORMATS
+
+func-inventory can be passed a --json or --xmlrpc parameter to override the default output format.  These
+output formats are much less readable in the git-produced diffs, but are more easily loaded by other programs
+that may want to "mine" the output of a func-inventory tree.   Using --json requires that the python-simplejson
+RPM be installed.
+
+=head1 ADDITONAL RESOURCES
+
+See https://hosted.fedoraproject.org/projects/func/ for more information.
+
+See also the manpages for "func", "funcd", "certmaster", and "certmaster-ca".
+
+=head1 AUTHOR
+
+Michael DeHaan <mdehaan@redhat.com>
+
+
+
diff --git a/docs/func.pod b/docs/func.pod
new file mode 100644 (file)
index 0000000..5ee594b
--- /dev/null
@@ -0,0 +1,111 @@
+=head1 NAME
+
+Func -- Fedora Unified Network Controller.
+
+=head1 SYNOPSIS
+
+func "*" list_minions
+
+func target.example.org call module method [args ...]
+
+func "target*.example.org" call module method [args ...]
+
+func "webserver1;mailserver2" call module method [args ...]
+
+=head1 DESCRIPTION
+
+"func" allows remote control of machines running funcd (called "minions")
+that are set to obey this machine (called the "overlord").  This includes
+performing various remote operations and gathering data.
+
+"func" can address multiple machines at the same time by specifying
+their names with globs, which follow shell glob syntax.
+
+See the project homepage (below) for a list of modules available
+and a more in-depth description of what each of them do.
+
+=head1 THE "CALL" MODULE
+
+The "call" module is used for running func modules remotely.  
+
+Format:  func "*.example.org" call <module> <function> [ args ... ]
+
+=head1 LISTING REMOTE MODULES AVAILABLE
+
+It's possible to ask func minions what modules they have installed:
+
+func "*.example.org" call system list_modules
+
+=head1 LISTING REMOTE FUNCTIONS AVAILABLE IN A MODULE
+
+It is also possible to ask remote func modules what functions they support:
+
+func target.example.org call modulename list_methods
+
+=head1 CALLING A REMOTE COMMAND
+
+Once you have the name of a module you want to run, use call to invoke it:
+
+func target.example.org call modulename methodname
+
+=head1 OUTPUT FORMATS
+
+The "call" command by default outputs data using a "pretty printer".  Other
+display options include --raw, --json, and --xmlrpc, which may be more
+desirable if you are running func inside another script or prefer to read
+those display formats.
+
+Example: func "*" call --json service inventory
+
+
+=head1 HELPER MODULES
+
+In addition to "call", there are other modules that make control of remote
+machines, as well as data display, more user friendly.  They augment "call"
+by providing some additional functionality.
+
+You will notice that the syntax for using one of these helper modules
+varies slightly from just using "call" directly.
+
+For example "show" can be used to show remote data.  The normal command "func '*'
+command would dump a very large amount of data, while the show command can mine
+only a few details.  This might make things more readable, for instance, when
+not going through the Python API (where you would not care). 
+
+func "*.example.org" show hardware --help
+
+func "*.example.org" show hardware systemMemory
+
+func "*.example.org" show hardware os
+
+Another useful helper command module is copyfile, which allows func to work like scp from 
+the shell, though it can address multiple systems at the same time.  
+
+The following example pushes one file out to multiple systems:
+
+func "*.example.org" copyfile --file=/tmp/foo --remotepath=/tmp/foo
+
+While these helper modules will grow over time, usage of "call" syntax 
+directly is fine also.  See the Wiki for more examples as they evolve.
+
+=head1 --verbose
+
+Use this flag to output extra information from func while it is running.
+All func commands can take this flag.
+
+=head1 EXIT_STATUS
+
+Func commands have return values that vary based on the module being
+called.  See the project page (linked below) for more information.
+
+=head1 ADDITONAL RESOURCES
+
+See https://hosted.fedoraproject.org/projects/func/ for more information, including information on scripting func from Python.
+
+See also the manpages for "func-inventory", "funcd", "certmaster", and "certmaster-ca".
+
+=head1 AUTHOR
+
+Various. See https://hosted.fedoraproject.org/projects/func
+
+
diff --git a/docs/funcd.pod b/docs/funcd.pod
new file mode 100644 (file)
index 0000000..da4ec75
--- /dev/null
@@ -0,0 +1,25 @@
+=head1 NAME
+
+funcd -- deaemon for the Fedora Universal Network Controller
+
+=head1 SYNOPSIS
+
+funcd (it's a daemon and takes no arguments)
+
+=head1 DESCRIPTION
+
+funcd registers itself to a certificate server (certmaster) listed in /etc/func/minion.conf and takes orders from the command line func when that program is run from that certificate server.  See /etc/func/minion.conf for other configuration options.
+
+Modules and capabilities provided by funcd are specified at https://hosted.fedoraproject.org/projects/func/
+
+=head1 ADDITONAL RESOURCES
+
+See https://hosted.fedoraproject.org/projects/func/.  It's a Wiki.
+
+See also the manpages for "func", "certmaster", and "certmaster-ca".
+
+=head1 AUTHOR
+
+Various. See https://hosted.fedoraproject.org/projects/func
+
+
diff --git a/etc/certmaster.conf b/etc/certmaster.conf
new file mode 100644 (file)
index 0000000..71b2068
--- /dev/null
@@ -0,0 +1,7 @@
+[main]
+listen_addr = 
+cadir = /etc/pki/func/ca
+certroot = /var/lib/func/certmaster/certs
+csrroot = /var/lib/func/certmaster/csrs
+autosign = no
+
diff --git a/etc/func_rotate b/etc/func_rotate
new file mode 100644 (file)
index 0000000..e12edfb
--- /dev/null
@@ -0,0 +1,19 @@
+/var/log/func/audit.log {
+   missingok
+   notifempty
+   rotate 4
+   weekly
+   postrotate
+      if [ -f /var/lock/subsys/funcd ]; then
+         /etc/init.d/funcd condrestart
+      fi
+   endscript
+}
+
+/var/log/func/func.log {
+   missingok
+   notifempty
+   rotate 4
+   weekly
+}
+
diff --git a/etc/minion.conf b/etc/minion.conf
new file mode 100644 (file)
index 0000000..f2e2b34
--- /dev/null
@@ -0,0 +1,8 @@
+# configuration for minions
+
+[main]
+log_level = DEBUG
+certmaster = certmaster
+cert_dir = /etc/pki/func
+acl_dir = /etc/func/minion-acl.d
+
diff --git a/etc/sample.acl b/etc/sample.acl
new file mode 100644 (file)
index 0000000..1a093a8
--- /dev/null
@@ -0,0 +1,5 @@
+#config file for minion Access control lists
+#this specifies which methods a connecting client is allowed to run
+# format is: cn-certificate-hash = method1, method2, method3
+# default allows the certmaster key to run all methods
+
diff --git a/func/CommonErrors.py b/func/CommonErrors.py
new file mode 100644 (file)
index 0000000..c76cb3d
--- /dev/null
@@ -0,0 +1,69 @@
+# 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 Library 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+#
+# Copyright 2005 Dan Williams <dcbw@redhat.com> and Red Hat, Inc.
+
+from exceptions import Exception
+
+def canIgnoreSSLError(e):
+    """
+    Identify common network errors that mean we cannot connect to the server
+    """
+
+    # This is a bit complicated by the fact that different versions of
+    # M2Crypto & OpenSSL seem to return different error codes for the
+    # same type of error
+    s = "%s" % e
+    if e[0] == 104:     # Connection refused
+        return True
+    elif e[0] == 111:   # Connection reset by peer
+        return True
+    elif e[0] == 61:    # Connection refused
+        return True
+    elif e[0] == 54:    # Connection reset by peer
+        return True
+    elif s == "no certificate returned":
+        return True
+    elif s == "wrong version number":
+        return True
+    elif s == "unexpected eof":
+        return True
+
+    return False
+
+
+def canIgnoreSocketError(e):
+    """
+    Identify common network errors that mean we cannot connect to the server
+    """
+
+    try:
+        if e[0] == 111:     # Connection refused
+            return True
+        elif e[0] == 104:   # Connection reset by peer
+            return True
+        elif e[0] == 61:    # Connection refused
+            return True
+    except IndexError:
+        return True
+
+    return False
+
+class Func_Client_Exception(Exception):
+    def __init__(self, value=None):
+        Exception.__init__(self)
+        self.value = value
+    def __str__(self):
+        return "%s" %(self.value,)
+
diff --git a/func/Makefile b/func/Makefile
new file mode 100755 (executable)
index 0000000..99fd546
--- /dev/null
@@ -0,0 +1,24 @@
+
+
+PYFILES                = $(wildcard *.py)
+PYDIRS = minion overlord
+
+PYCHECKER       = /usr/bin/pychecker
+PYFLAKES       = /usr/bin/pyflakes
+
+clean::
+       @rm -fv *.pyc *~ .*~ *.pyo 
+       @find . -name .\#\* -exec rm -fv {} \;
+       @rm -fv *.rpm
+       
+
+pychecker:: 
+       @$(PYCHECKER) $(PYFILES) || exit 0
+
+pyflakes::
+       @$(PYFLAKES) $(PYFILES) || exit 0
+
+pychecker::
+       -for d in $(PYDIRS); do ($(MAKE) -C $$d pychecker ); done   
+pyflakes::
+       -for d in $(PYDIRS); do ($(MAKE) -C $$d pyflakes ); done        
diff --git a/func/SSLCommon.py b/func/SSLCommon.py
new file mode 100644 (file)
index 0000000..6959749
--- /dev/null
@@ -0,0 +1,121 @@
+# 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 Library 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+#
+# Copyright 2005 Dan Williams <dcbw@redhat.com> and Red Hat, Inc.
+
+import os, sys
+from OpenSSL import SSL
+import SSLConnection
+import httplib
+import socket
+import SocketServer
+
+def our_verify(connection, x509, errNum, errDepth, preverifyOK):
+    # print "Verify: errNum = %s, errDepth = %s, preverifyOK = %s" % (errNum, errDepth, preverifyOK)
+
+    # preverifyOK should tell us whether or not the client's certificate
+    # correctly authenticates against the CA chain
+    return preverifyOK
+
+
+def CreateSSLContext(pkey, cert, ca_cert):
+    for f in pkey, cert, ca_cert:
+        if f and not os.access(f, os.R_OK):
+            print "%s does not exist or is not readable." % f
+            os._exit(1)
+
+    ctx = SSL.Context(SSL.SSLv3_METHOD)   # SSLv3 only
+    ctx.use_certificate_file(cert)
+    ctx.use_privatekey_file(pkey)
+    ctx.load_client_ca(ca_cert)
+    ctx.load_verify_locations(ca_cert)
+    verify = SSL.VERIFY_PEER | SSL.VERIFY_FAIL_IF_NO_PEER_CERT
+    ctx.set_verify(verify, our_verify)
+    ctx.set_verify_depth(10)
+    ctx.set_options(SSL.OP_NO_SSLv2 | SSL.OP_NO_TLSv1)
+    return ctx
+
+
+
+class BaseServer(SocketServer.TCPServer):
+    allow_reuse_address = 1
+
+    def __init__(self, server_addr, req_handler):
+        self._quit = False
+        self.allow_reuse_address = 1
+        SocketServer.TCPServer.__init__(self, server_addr, req_handler)
+
+    def stop(self):
+        self._quit = True
+
+    def serve_forever(self):
+        while not self._quit:
+            self.handle_request()
+        self.server_close()
+
+
+class BaseSSLServer(BaseServer):
+    """ SSL-enabled variant """
+
+    def __init__(self, server_address, req_handler, pkey, cert, ca_cert, timeout=None):
+        self._timeout = timeout
+        self.ssl_ctx = CreateSSLContext(pkey, cert, ca_cert)
+
+        BaseServer.__init__(self, server_address, req_handler)
+
+        sock = socket.socket(self.address_family, self.socket_type)
+        con = SSL.Connection(self.ssl_ctx, sock)
+        self.socket = SSLConnection.SSLConnection(con)
+        if sys.version_info[:3] >= (2, 3, 0):
+            self.socket.settimeout(self._timeout)
+        self.server_bind()
+        self.server_activate()
+
+        host, port = self.socket.getsockname()[:2]
+        self.server_name = socket.getfqdn(host)
+        self.server_port = port
+
+
+class HTTPSConnection(httplib.HTTPConnection):
+    "This class allows communication via SSL."
+
+    response_class = httplib.HTTPResponse
+
+    def __init__(self, host, port=None, ssl_context=None, strict=None, timeout=None):
+        httplib.HTTPConnection.__init__(self, host, port, strict)
+        self.ssl_ctx = ssl_context
+        self._timeout = timeout
+
+    def connect(self):
+        sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+        con = SSL.Connection(self.ssl_ctx, sock)
+        self.sock = SSLConnection.SSLConnection(con)
+        if sys.version_info[:3] >= (2, 3, 0):
+            self.sock.settimeout(self._timeout)
+        self.sock.connect((self.host, self.port))
+
+
+class HTTPS(httplib.HTTP):
+    """Compatibility with 1.5 httplib interface
+
+    Python 1.5.2 did not have an HTTPS class, but it defined an
+    interface for sending http requests that is also useful for
+    https.
+    """
+
+    _connection_class = HTTPSConnection
+
+    def __init__(self, host='', port=None, ssl_context=None, strict=None, timeout=None):
+        self._setup(self._connection_class(host, port, ssl_context, strict, timeout))
+
diff --git a/func/SSLConnection.py b/func/SSLConnection.py
new file mode 100644 (file)
index 0000000..98ed8a0
--- /dev/null
@@ -0,0 +1,165 @@
+# Higher-level SSL objects used by rpclib
+#
+# Copyright (c) 2002 Red Hat, Inc.
+#
+# Author: Mihai Ibanescu <misa@redhat.com>
+# Modifications by Dan Williams <dcbw@redhat.com>
+
+
+from OpenSSL import SSL
+import time, socket, select
+from func.CommonErrors import canIgnoreSSLError
+
+
+class SSLConnection:
+    """
+    This whole class exists just to filter out a parameter
+    passed in to the shutdown() method in SimpleXMLRPC.doPOST()
+    """
+
+    DEFAULT_TIMEOUT = 20
+
+    def __init__(self, conn):
+        """
+        Connection is not yet a new-style class,
+        so I'm making a proxy instead of subclassing.
+        """
+        self.__dict__["conn"] = conn
+        self.__dict__["close_refcount"] = 0
+        self.__dict__["closed"] = False
+        self.__dict__["timeout"] = self.DEFAULT_TIMEOUT
+
+    def __del__(self):
+        self.__dict__["conn"].close()
+
+    def __getattr__(self,name):
+        return getattr(self.__dict__["conn"], name)
+
+    def __setattr__(self,name, value):
+        setattr(self.__dict__["conn"], name, value)
+
+    def settimeout(self, timeout):
+        if timeout == None:
+            self.__dict__["timeout"] = self.DEFAULT_TIMEOUT
+        else:
+            self.__dict__["timeout"] = timeout
+        self.__dict__["conn"].settimeout(timeout)
+
+    def shutdown(self, how=1):
+        """
+        SimpleXMLRpcServer.doPOST calls shutdown(1),
+        and Connection.shutdown() doesn't take
+        an argument. So we just discard the argument.
+        """
+        self.__dict__["conn"].shutdown()
+
+    def accept(self):
+        """
+        This is the other part of the shutdown() workaround.
+        Since servers create new sockets, we have to infect
+        them with our magic. :)
+        """
+        c, a = self.__dict__["conn"].accept()
+        return (SSLConnection(c), a)
+
+    def makefile(self, mode, bufsize):
+        """
+        We need to use socket._fileobject Because SSL.Connection
+        doesn't have a 'dup'. Not exactly sure WHY this is, but
+        this is backed up by comments in socket.py and SSL/connection.c
+
+        Since httplib.HTTPSResponse/HTTPConnection depend on the
+        socket being duplicated when they close it, we refcount the
+        socket object and don't actually close until its count is 0.
+        """
+        self.__dict__["close_refcount"] = self.__dict__["close_refcount"] + 1
+        return PlgFileObject(self, mode, bufsize)
+
+    def close(self):
+        if self.__dict__["closed"]:
+            return
+        self.__dict__["close_refcount"] = self.__dict__["close_refcount"] - 1
+        if self.__dict__["close_refcount"] == 0:
+            self.shutdown()
+            self.__dict__["conn"].close()
+            self.__dict__["closed"] = True
+
+    def sendall(self, data, flags=0):
+        """
+        - Use select() to simulate a socket timeout without setting the socket
+            to non-blocking mode.
+        - Don't use pyOpenSSL's sendall() either, since it just loops on WantRead
+            or WantWrite, consuming 100% CPU, and never times out.
+        """
+        timeout = self.__dict__["timeout"]
+        con = self.__dict__["conn"]
+        (read, write, excpt) = select.select([], [con], [], timeout)
+        if not con in write:
+            raise socket.timeout((110, "Operation timed out."))
+
+        starttime = time.time()
+        origlen = len(data)
+        sent = -1
+        while len(data):
+            curtime = time.time()
+            if curtime - starttime > timeout:
+                raise socket.timeout((110, "Operation timed out."))
+
+            try:
+                sent = con.send(data, flags)
+            except SSL.SysCallError, e:
+                if e[0] == 32:      # Broken Pipe
+                    self.close()
+                    sent = 0
+                else:
+                    raise socket.error(e)
+            except (SSL.WantWriteError, SSL.WantReadError):
+                time.sleep(0.2)
+                continue
+
+            data = data[sent:]
+        return origlen - len(data)
+
+    def recv(self, bufsize, flags=0):
+        """
+        Use select() to simulate a socket timeout without setting the socket
+        to non-blocking mode
+        """
+        timeout = self.__dict__["timeout"]
+        con = self.__dict__["conn"]
+        (read, write, excpt) = select.select([con], [], [], timeout)
+        if not con in read:
+            raise socket.timeout((110, "Operation timed out."))
+
+        starttime = time.time()
+        while True:
+            curtime = time.time()
+            if curtime - starttime > timeout:
+                raise socket.timeout((110, "Operation timed out."))
+
+            try:
+                return con.recv(bufsize, flags)
+            except SSL.ZeroReturnError:
+                return None
+            except SSL.WantReadError:
+                time.sleep(0.2)
+            except Exception, e:
+                if canIgnoreSSLError(e):
+                    return None
+                else:
+                    raise e
+        return None
+
+
+class PlgFileObject(socket._fileobject):
+    def close(self):
+        """
+        socket._fileobject doesn't actually _close_ the socket,
+        which we want it to do, so we have to override.
+        """
+        try:
+            if self._sock:
+                self.flush()
+                self._sock.close()
+        finally:
+            self._sock = None
diff --git a/func/__init__.py b/func/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/func/certmaster.py b/func/certmaster.py
new file mode 100755 (executable)
index 0000000..fe5dcbc
--- /dev/null
@@ -0,0 +1,247 @@
+# FIXME: more intelligent fault raises
+
+"""
+cert master listener
+
+Copyright 2007, Red Hat, Inc
+see AUTHORS
+
+This software may be freely redistributed under the terms of the GNU
+general public license.
+
+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., 675 Mass Ave, Cambridge, MA 02139, USA.
+"""
+
+# standard modules
+import SimpleXMLRPCServer
+import sys
+import os
+import os.path
+from OpenSSL import crypto
+import sha
+import glob
+import socket
+import exceptions
+
+#from func.server import codes
+import certs
+import codes
+import utils
+from config import read_config
+from commonconfig import CMConfig
+
+CERTMASTER_LISTEN_PORT = 51235
+CERTMASTER_CONFIG = "/etc/func/certmaster.conf"
+
+class CertMaster(object):
+    def __init__(self, conf_file=CERTMASTER_CONFIG):
+        self.cfg = read_config(conf_file, CMConfig)
+
+        usename = utils.get_hostname()
+
+        mycn = '%s-CA-KEY' % usename
+        self.ca_key_file = '%s/funcmaster.key' % self.cfg.cadir
+        self.ca_cert_file = '%s/funcmaster.crt' % self.cfg.cadir
+        try:
+            if not os.path.exists(self.cfg.cadir):
+                os.makedirs(self.cfg.cadir)
+            if not os.path.exists(self.ca_key_file) and not os.path.exists(self.ca_cert_file):
+                certs.create_ca(CN=mycn, ca_key_file=self.ca_key_file, ca_cert_file=self.ca_cert_file)
+        except (IOError, OSError), e:
+            print 'Cannot make certmaster certificate authority keys/certs, aborting: %s' % e
+            sys.exit(1)
+
+            
+        # open up the cakey and cacert so we have them available
+        self.cakey = certs.retrieve_key_from_file(self.ca_key_file)
+        self.cacert = certs.retrieve_cert_from_file(self.ca_cert_file)
+        
+        for dirpath in [self.cfg.cadir, self.cfg.certroot, self.cfg.csrroot]:
+            if not os.path.exists(dirpath):
+                os.makedirs(dirpath)
+
+        # setup handlers
+        self.handlers = {
+                 'wait_for_cert': self.wait_for_cert,
+                 }
+        
+    def _dispatch(self, method, params):
+        if method == 'trait_names' or method == '_getAttributeNames':
+            return self.handlers.keys()
+        
+        if method in self.handlers.keys():
+            return self.handlers[method](*params)
+        else:
+            raise codes.InvalidMethodException
+    
+    def _sanitize_cn(self, commonname):
+        commonname = commonname.replace('/', '')
+        commonname = commonname.replace('\\', '')       
+        return commonname
+    
+    def wait_for_cert(self, csrbuf):
+        """
+           takes csr as a string
+           returns True, caller_cert, ca_cert
+           returns False, '', ''
+        """
+       
+        try:
+            csrreq = crypto.load_certificate_request(crypto.FILETYPE_PEM, csrbuf)
+        except crypto.Error, e:
+            #XXX need to raise a fault here and document it - but false is just as good
+            return False, '', ''
+            
+        requesting_host = self._sanitize_cn(csrreq.get_subject().CN)
+        
+        # get rid of dodgy characters in the filename we're about to make
+        
+        certfile = '%s/%s.cert' % (self.cfg.certroot, requesting_host)
+        csrfile = '%s/%s.csr' % (self.cfg.csrroot, requesting_host)
+
+        # check for old csr on disk
+        # if we have it - compare the two - if they are not the same - raise a fault
+        if os.path.exists(csrfile):
+            oldfo = open(csrfile)
+            oldcsrbuf = oldfo.read()
+            oldsha = sha.new()
+            oldsha.update(oldcsrbuf)
+            olddig = oldsha.hexdigest()
+            newsha = sha.new()
+            newsha.update(csrbuf)
+            newdig = newsha.hexdigest()
+            if not newdig == olddig:
+                # XXX raise a proper fault
+                return False, '', ''
+
+        # look for a cert:
+        # if we have it, then return True, etc, etc
+        if os.path.exists(certfile):
+            slavecert = certs.retrieve_cert_from_file(certfile)
+            cert_buf = crypto.dump_certificate(crypto.FILETYPE_PEM, slavecert)
+            cacert_buf = crypto.dump_certificate(crypto.FILETYPE_PEM, self.cacert)
+            return True, cert_buf, cacert_buf
+        
+        # if we don't have a cert then:
+        # if we're autosign then sign it, write out the cert and return True, etc, etc
+        # else write out the csr
+        
+        if self.cfg.autosign:
+            cert_fn = self.sign_this_csr(csrreq)
+            cert = certs.retrieve_cert_from_file(cert_fn)            
+            cert_buf = crypto.dump_certificate(crypto.FILETYPE_PEM, cert)
+            cacert_buf = crypto.dump_certificate(crypto.FILETYPE_PEM, self.cacert)
+            return True, cert_buf, cacert_buf
+        
+        else:
+            # write the csr out to a file to be dealt with by the admin
+            destfo = open(csrfile, 'w')
+            destfo.write(crypto.dump_certificate_request(crypto.FILETYPE_PEM, csrreq))
+            destfo.close()
+            del destfo
+            return False, '', ''
+
+        return False, '', ''
+
+    def get_csrs_waiting(self):
+        hosts = [] 
+        csrglob = '%s/*.csr' % self.cfg.csrroot
+        csr_list = glob.glob(csrglob)
+        for f in csr_list:
+            hn = os.path.basename(f)
+            hn = hn[:-4]
+            hosts.append(hn)
+        return hosts
+   
+    def remove_this_cert(self, hn):
+        """ removes cert for hostname using unlink """
+        cm = self
+        csrglob = '%s/%s.csr' % (cm.cfg.csrroot, hn)
+        csrs = glob.glob(csrglob)
+        certglob = '%s/%s.cert' % (cm.cfg.certroot, hn)
+        certs = glob.glob(certglob)
+        if not csrs and not certs:
+            # FIXME: should be an exception?
+            print 'No match for %s to clean up' % hn
+            return
+        for fn in csrs + certs:
+            print 'Cleaning out %s for host matching %s' % (fn, hn)
+            os.unlink(fn)         
+            
+    def sign_this_csr(self, csr):
+        """returns the path to the signed cert file"""
+        csr_unlink_file = None
+
+        if type(csr) is type(''): 
+            if csr.startswith('/') and os.path.exists(csr):  # we have a full path to the file
+                csrfo = open(csr)
+                csr_buf = csrfo.read()
+                csr_unlink_file = csr
+                
+            elif os.path.exists('%s/%s' % (self.cfg.csrroot, csr)): # we have a partial path?
+                csrfo = open('%s/%s' % (self.cfg.csrroot, csr))
+                csr_buf = csrfo.read()
+                csr_unlink_file = '%s/%s' % (self.cfg.csrroot, csr)
+                
+            # we have a string of some kind
+            else:
+                csr_buf = csr
+
+            try:
+                csrreq = crypto.load_certificate_request(crypto.FILETYPE_PEM, csr_buf)                
+            except crypto.Error, e:
+                raise exceptions.Exception("Bad CSR: %s" % csr)
+                
+        else: # assume we got a bare csr req
+            csrreq = csr
+        requesting_host = self._sanitize_cn(csrreq.get_subject().CN)
+        
+        certfile = '%s/%s.cert' % (self.cfg.certroot, requesting_host)
+        thiscert = certs.create_slave_certificate(csrreq, self.cakey, self.cacert, self.cfg.cadir)
+        destfo = open(certfile, 'w')
+        destfo.write(crypto.dump_certificate(crypto.FILETYPE_PEM, thiscert))
+        destfo.close()
+        del destfo
+        if csr_unlink_file and os.path.exists(csr_unlink_file):
+            os.unlink(csr_unlink_file)
+            
+        return certfile
+        
+
+class CertmasterXMLRPCServer(SimpleXMLRPCServer.SimpleXMLRPCServer):
+    def __init__(self, args):
+        self.allow_reuse_address = True
+        SimpleXMLRPCServer.SimpleXMLRPCServer.__init__(self, args)
+        
+
+def serve(xmlrpcinstance):
+
+    """
+    Code for starting the XMLRPC service.
+    """
+
+    server = CertmasterXMLRPCServer((xmlrpcinstance.cfg.listen_addr, CERTMASTER_LISTEN_PORT))
+    server.logRequests = 0 # don't print stuff to console
+    server.register_instance(xmlrpcinstance)
+    server.serve_forever()
+
+
+def main(argv):
+    
+    cm = CertMaster('/etc/func/certmaster.conf')
+
+    if "daemon" in argv or "--daemon" in argv:
+        utils.daemonize("/var/run/certmaster.pid")
+    else:
+        print "serving...\n"
+
+
+    # just let exceptions bubble up for now
+    serve(cm)
+
+if __name__ == "__main__":
+    #textdomain(I18N_DOMAIN)
+    main(sys.argv)
diff --git a/func/certs.py b/func/certs.py
new file mode 100644 (file)
index 0000000..4d6bf15
--- /dev/null
@@ -0,0 +1,139 @@
+# 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 Library 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+# Copyright (c) 2007 Red Hat, inc 
+#- Written by Seth Vidal skvidal @ fedoraproject.org
+
+from OpenSSL import crypto
+import socket
+import os
+import utils
+
+def_country = 'UN'
+def_state = 'FC'
+def_local = 'Func-ytown'
+def_org = 'func'
+def_ou = 'slave-key'
+
+
+def make_keypair(dest=None):
+    pkey = crypto.PKey()
+    pkey.generate_key(crypto.TYPE_RSA, 2048)
+    if dest:
+        destfd = os.open(dest, os.O_RDWR|os.O_CREAT, 0600)
+        os.write(destfd, (crypto.dump_privatekey(crypto.FILETYPE_PEM, pkey)))
+        os.close(destfd)
+    
+    return pkey
+
+
+def make_csr(pkey, dest=None, cn=None):
+    req = crypto.X509Req()
+    req.get_subject()
+    subj  = req.get_subject()
+    subj.C = def_country
+    subj.ST = def_state
+    subj.L = def_local
+    subj.O = def_org
+    subj.OU = def_ou
+    if cn:
+        subj.CN = cn
+    else:
+        subj.CN = utils.get_hostname() 
+    subj.emailAddress = 'root@%s' % subj.CN       
+        
+    req.set_pubkey(pkey)
+    req.sign(pkey, 'md5')
+    if dest:
+        destfd = os.open(dest, os.O_RDWR|os.O_CREAT, 0644)
+        os.write(destfd, crypto.dump_certificate_request(crypto.FILETYPE_PEM, req))
+        os.close(destfd)
+
+    return req
+
+
+def retrieve_key_from_file(keyfile):
+    fo = open(keyfile, 'r')
+    buf = fo.read()
+    keypair = crypto.load_privatekey(crypto.FILETYPE_PEM, buf)
+    return keypair
+
+    
+def retrieve_csr_from_file(csrfile):
+    fo = open(csrfile, 'r')
+    buf = fo.read()
+    csrreq = crypto.load_certificate_request(crypto.FILETYPE_PEM, buf)
+    return csrreq
+
+
+def retrieve_cert_from_file(certfile):
+    fo = open(certfile, 'r')
+    buf = fo.read()
+    cert = crypto.load_certificate(crypto.FILETYPE_PEM, buf)
+    return cert
+
+
+def create_ca(CN="Func Certificate Authority", ca_key_file=None, ca_cert_file=None):
+    cakey = make_keypair(dest=ca_key_file)
+    careq = make_csr(cakey, cn=CN)
+    cacert = crypto.X509()
+    cacert.set_serial_number(0)
+    cacert.gmtime_adj_notBefore(0)
+    cacert.gmtime_adj_notAfter(60*60*24*365*10) # 10 yrs - hard to beat this kind of cert!
+    cacert.set_issuer(careq.get_subject())
+    cacert.set_subject(careq.get_subject())
+    cacert.set_pubkey(careq.get_pubkey())
+    cacert.sign(cakey, 'md5')
+    if ca_cert_file:
+        destfo = open(ca_cert_file, 'w')
+        destfo.write(crypto.dump_certificate(crypto.FILETYPE_PEM, cacert))
+        destfo.close()
+                                           
+def _get_serial_number(cadir):
+    serial = '%s/serial.txt' % cadir
+    i = 1
+    if os.path.exists(serial):
+        f = open(serial, 'r').read()
+        f = f.replace('\n','')
+        try:
+            i = int(f)
+            i+=1      
+        except ValueError, e:
+            i = 1
+            
+    _set_serial_number(cadir, i)        
+    return i
+
+
+def _set_serial_number(cadir, last):
+    serial = '%s/serial.txt' % cadir
+    f = open(serial, 'w')
+    f.write(str(last) + '\n')
+    f.close()
+            
+        
+def create_slave_certificate(csr, cakey, cacert, cadir, slave_cert_file=None):
+    cert = crypto.X509()
+    cert.set_serial_number(_get_serial_number(cadir))
+    cert.gmtime_adj_notBefore(0)
+    cert.gmtime_adj_notAfter(60*60*24*365*10) # 10 yrs - hard to beat this kind of cert!
+    cert.set_issuer(cacert.get_subject())
+    cert.set_subject(csr.get_subject())
+    cert.set_pubkey(csr.get_pubkey())
+    cert.sign(cakey, 'md5')
+    if slave_cert_file:
+        destfo = open(slave_cert_file, 'w')
+        destfo.write(crypto.dump_certificate(crypto.FILETYPE_PEM, cert))
+        destfo.close()
+    return cert
diff --git a/func/codes.py b/func/codes.py
new file mode 100755 (executable)
index 0000000..c6bcb61
--- /dev/null
@@ -0,0 +1,25 @@
+"""
+func
+
+Copyright 2007, Red Hat, Inc
+See AUTHORS
+
+This software may be freely redistributed under the terms of the GNU
+general public license.
+
+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., 675 Mass Ave, Cambridge, MA 02139, USA.
+"""
+
+import exceptions
+
+
+class FuncException(exceptions.Exception):
+    pass
+
+
+class InvalidMethodException(FuncException):
+    pass
+
+# FIXME: more sub-exceptions maybe
diff --git a/func/commonconfig.py b/func/commonconfig.py
new file mode 100644 (file)
index 0000000..9fd3356
--- /dev/null
@@ -0,0 +1,15 @@
+from config import BaseConfig, BoolOption, IntOption, Option
+
+class CMConfig(BaseConfig):
+    listen_addr = Option('')
+    cadir = Option('/etc/pki/func/ca')
+    certroot =  Option('/var/lib/func/certmaster/certs')
+    csrroot = Option('/var/lib/func/certmaster/csrs')
+    autosign = BoolOption(False)
+
+
+class FuncdConfig(BaseConfig):
+    log_level = Option('INFO')
+    certmaster = Option('certmaster')
+    cert_dir = Option('/etc/pki/func')
+    acl_dir = Option('/etc/func/minion-acl.d')
diff --git a/func/config.py b/func/config.py
new file mode 100644 (file)
index 0000000..8202457
--- /dev/null
@@ -0,0 +1,478 @@
+# 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 Library 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+# Copyright 2002 Duke University 
+# filched from yum  - menno smits wrote this - he rocks
+
+
+import os
+import sys
+import warnings
+import copy
+import urlparse
+from ConfigParser import NoSectionError, NoOptionError, ConfigParser
+from ConfigParser import ParsingError
+import exceptions
+
+CONFIG_FILE = "/etc/func/certmaster.conf"
+
+class ConfigError(exceptions.Exception):
+    def __init__(self, value=None):
+        exceptions.Exception.__init__(self)
+        self.value = value
+    def __str__(self):
+        return "%s" %(self.value,)
+   
+class Option(object):
+    '''
+    This class handles a single Yum configuration file option. Create
+    subclasses for each type of supported configuration option.
+    
+    Python descriptor foo (__get__ and __set__) is used to make option
+    definition easy and consise.
+    '''
+
+    def __init__(self, default=None):
+        self._setattrname()
+        self.inherit = False
+        self.default = default
+
+    def _setattrname(self):
+        '''Calculate the internal attribute name used to store option state in
+        configuration instances.
+        '''
+        self._attrname = '__opt%d' % id(self)
+
+    def __get__(self, obj, objtype):
+        '''Called when the option is read (via the descriptor protocol). 
+
+        @param obj: The configuration instance to modify.
+        @param objtype: The type of the config instance (not used).
+        @return: The parsed option value or the default value if the value
+            wasn't set in the configuration file.
+        '''
+        if obj is None:
+            return self
+
+        return getattr(obj, self._attrname, None)
+
+    def __set__(self, obj, value):
+        '''Called when the option is set (via the descriptor protocol). 
+
+        @param obj: The configuration instance to modify.
+        @param value: The value to set the option to.
+        @return: Nothing.
+        '''
+        # Only try to parse if its a string
+        if isinstance(value, basestring):
+            try:
+                value = self.parse(value)
+            except ValueError, e:
+                # Add the field name onto the error
+                raise ValueError('Error parsing %r: %s' % (value, str(e)))
+
+        setattr(obj, self._attrname, value)
+
+    def setup(self, obj, name):
+        '''Initialise the option for a config instance. 
+        This must be called before the option can be set or retrieved. 
+
+        @param obj: BaseConfig (or subclass) instance.
+        @param name: Name of the option.
+        '''
+        setattr(obj, self._attrname, copy.copy(self.default))
+
+    def clone(self):
+        '''Return a safe copy of this Option instance
+        '''
+        new = copy.copy(self)
+        new._setattrname()
+        return new
+
+    def parse(self, s):
+        '''Parse the string value to the Option's native value.
+
+        @param s: Raw string value to parse.
+        @return: Validated native value.
+    
+        Will raise ValueError if there was a problem parsing the string.
+        Subclasses should override this.
+        '''
+        return s
+
+    def tostring(self, value):
+        '''Convert the Option's native value to a string value.
+
+        @param value: Native option value.
+        @return: String representation of input.
+
+        This does the opposite of the parse() method above.
+        Subclasses should override this.
+        '''
+        return str(value)
+
+
+def Inherit(option_obj):
+    '''Clone an Option instance for the purposes of inheritance. The returned
+    instance has all the same properties as the input Option and shares items
+    such as the default value. Use this to avoid redefinition of reused
+    options.
+
+    @param option_obj: Option instance to inherit.
+    @return: New Option instance inherited from the input.
+    '''
+    new_option = option_obj.clone()
+    new_option.inherit = True
+    return new_option
+
+
+class ListOption(Option):
+
+    def __init__(self, default=None):
+        if default is None:
+            default = []
+        super(ListOption, self).__init__(default)
+
+    def parse(self, s):
+        """Converts a string from the config file to a workable list
+
+        Commas and spaces are used as separators for the list
+        """
+        # we need to allow for the '\n[whitespace]' continuation - easier
+        # to sub the \n with a space and then read the lines
+        s = s.replace('\n', ' ')
+        s = s.replace(',', ' ')
+        return s.split()
+
+    def tostring(self, value):
+        return '\n '.join(value)
+
+
+class UrlOption(Option):
+    '''
+    This option handles lists of URLs with validation of the URL scheme.
+    '''
+
+    def __init__(self, default=None, schemes=('http', 'ftp', 'file', 'https'), 
+            allow_none=False):
+        super(UrlOption, self).__init__(default)
+        self.schemes = schemes
+        self.allow_none = allow_none
+
+    def parse(self, url):
+        url = url.strip()
+
+        # Handle the "_none_" special case
+        if url.lower() == '_none_':
+            if self.allow_none:
+                return None
+            else:
+                raise ValueError('"_none_" is not a valid value')
+
+        # Check that scheme is valid
+        (s,b,p,q,f,o) = urlparse.urlparse(url)
+        if s not in self.schemes:
+            raise ValueError('URL must be %s not "%s"' % (self._schemelist(), s))
+
+        return url
+
+    def _schemelist(self):
+        '''Return a user friendly list of the allowed schemes
+        '''
+        if len(self.schemes) < 1:
+            return 'empty'
+        elif len(self.schemes) == 1:
+            return self.schemes[0]
+        else:
+            return '%s or %s' % (', '.join(self.schemes[:-1]), self.schemes[-1])
+
+
+class UrlListOption(ListOption):
+    '''
+    Option for handling lists of URLs with validation of the URL scheme.
+    '''
+
+    def __init__(self, default=None, schemes=('http', 'ftp', 'file', 'https')):
+        super(UrlListOption, self).__init__(default)
+
+        # Hold a UrlOption instance to assist with parsing
+        self._urloption = UrlOption(schemes=schemes)
+        
+    def parse(self, s):
+        out = []
+        for url in super(UrlListOption, self).parse(s):
+            out.append(self._urloption.parse(url))
+        return out
+
+
+class IntOption(Option):
+    def parse(self, s):
+        try:
+            return int(s)
+        except (ValueError, TypeError), e:
+            raise ValueError('invalid integer value')
+
+
+class BoolOption(Option):
+    def parse(self, s):
+        s = s.lower()
+        if s in ('0', 'no', 'false'):
+            return False
+        elif s in ('1', 'yes', 'true'):
+            return True
+        else:
+            raise ValueError('invalid boolean value')
+
+    def tostring(self, value):
+        if value:
+            return "1"
+        else:
+            return "0"
+
+
+class FloatOption(Option):
+    def parse(self, s):
+        try:
+            return float(s.strip())
+        except (ValueError, TypeError):
+            raise ValueError('invalid float value')
+
+
+class SelectionOption(Option):
+    '''Handles string values where only specific values are allowed
+    '''
+    def __init__(self, default=None, allowed=()):
+        super(SelectionOption, self).__init__(default)
+        self._allowed = allowed
+        
+    def parse(self, s):
+        if s not in self._allowed:
+            raise ValueError('"%s" is not an allowed value' % s)
+        return s
+
+class BytesOption(Option):
+
+    # Multipliers for unit symbols
+    MULTS = {
+        'k': 1024,
+        'm': 1024*1024,
+        'g': 1024*1024*1024,
+    }
+
+    def parse(self, s):
+        """Parse a friendly bandwidth option to bytes
+
+        The input should be a string containing a (possibly floating point)
+        number followed by an optional single character unit. Valid units are
+        'k', 'M', 'G'. Case is ignored.
+       
+        Valid inputs: 100, 123M, 45.6k, 12.4G, 100K, 786.3, 0
+        Invalid inputs: -10, -0.1, 45.6L, 123Mb
+
+        Return value will always be an integer
+
+        1k = 1024 bytes.
+
+        ValueError will be raised if the option couldn't be parsed.
+        """
+        if len(s) < 1:
+            raise ValueError("no value specified")
+
+        if s[-1].isalpha():
+            n = s[:-1]
+            unit = s[-1].lower()
+            mult = self.MULTS.get(unit, None)
+            if not mult:
+                raise ValueError("unknown unit '%s'" % unit)
+        else:
+            n = s
+            mult = 1
+             
+        try:
+            n = float(n)
+        except ValueError:
+            raise ValueError("couldn't convert '%s' to number" % n)
+
+        if n < 0:
+            raise ValueError("bytes value may not be negative")
+
+        return int(n * mult)
+
+
+class ThrottleOption(BytesOption):
+
+    def parse(self, s):
+        """Get a throttle option. 
+
+        Input may either be a percentage or a "friendly bandwidth value" as
+        accepted by the BytesOption.
+
+        Valid inputs: 100, 50%, 80.5%, 123M, 45.6k, 12.4G, 100K, 786.0, 0
+        Invalid inputs: 100.1%, -4%, -500
+
+        Return value will be a int if a bandwidth value was specified or a
+        float if a percentage was given.
+
+        ValueError will be raised if input couldn't be parsed.
+        """
+        if len(s) < 1:
+            raise ValueError("no value specified")
+
+        if s[-1] == '%':
+            n = s[:-1]
+            try:
+                n = float(n)
+            except ValueError:
+                raise ValueError("couldn't convert '%s' to number" % n)
+            if n < 0 or n > 100:
+                raise ValueError("percentage is out of range")
+            return n / 100.0
+        else:
+            return BytesOption.parse(self, s)
+
+
+class BaseConfig(object):
+    '''
+    Base class for storing configuration definitions. Subclass when creating
+    your own definitons.
+    '''
+
+    def __init__(self):
+        self._section = None
+
+        for name in self.iterkeys():
+            option = self.optionobj(name)
+            option.setup(self, name)
+
+    def __str__(self):
+        out = []
+        out.append('[%s]' % self._section)
+        for name, value in self.iteritems():
+            out.append('%s: %r' % (name, value))
+        return '\n'.join(out)
+
+    def populate(self, parser, section, parent=None):
+        '''Set option values from a INI file section.
+
+        @param parser: ConfParser instance (or subclass)
+        @param section: INI file section to read use.
+        @param parent: Optional parent BaseConfig (or subclass) instance to use
+            when doing option value inheritance.
+        '''
+        self.cfg = parser
+        self._section = section
+
+        for name in self.iterkeys():
+            option = self.optionobj(name)
+            value = None
+            try:
+                value = parser.get(section, name)
+            except (NoSectionError, NoOptionError):
+                # No matching option in this section, try inheriting
+                if parent and option.inherit:
+                    value = getattr(parent, name)
+               
+            if value is not None:
+                setattr(self, name, value)
+
+    def optionobj(cls, name):
+        '''Return the Option instance for the given name
+        '''
+        obj = getattr(cls, name, None)
+        if isinstance(obj, Option):
+            return obj
+        else:
+            raise KeyError
+    optionobj = classmethod(optionobj)
+
+    def isoption(cls, name):
+        '''Return True if the given name refers to a defined option 
+        '''
+        try:
+            cls.optionobj(name)
+            return True
+        except KeyError:
+            return False
+    isoption = classmethod(isoption)
+
+    def iterkeys(self):
+        '''Yield the names of all defined options in the instance.
+        '''
+        for name, item in self.iteritems():
+            yield name
+
+    def iteritems(self):
+        '''Yield (name, value) pairs for every option in the instance.
+
+        The value returned is the parsed, validated option value.
+        '''
+        # Use dir() so that we see inherited options too
+        for name in dir(self):
+            if self.isoption(name):
+                yield (name, getattr(self, name))
+
+    def write(self, fileobj, section=None, always=()):
+        '''Write out the configuration to a file-like object
+
+        @param fileobj: File-like object to write to
+        @param section: Section name to use. If not-specified the section name
+            used during parsing will be used.
+        @param always: A sequence of option names to always write out.
+            Options not listed here will only be written out if they are at
+            non-default values. Set to None to dump out all options.
+        '''
+        # Write section heading
+        if section is None:
+            if self._section is None:
+                raise ValueError("not populated, don't know section")
+            section = self._section
+
+        # Updated the ConfigParser with the changed values    
+        cfgOptions = self.cfg.options(section)
+        for name,value in self.iteritems():
+            option = self.optionobj(name)
+            if always is None or name in always or option.default != value or name in cfgOptions :
+                self.cfg.set(section,name, option.tostring(value))
+        # write the updated ConfigParser to the fileobj.
+        self.cfg.write(fileobj)
+
+    def getConfigOption(self, option, default=None):
+        warnings.warn('getConfigOption() will go away in a future version of Yum.\n'
+                'Please access option values as attributes or using getattr().',
+                DeprecationWarning)
+        if hasattr(self, option):
+            return getattr(self, option)
+        return default
+
+    def setConfigOption(self, option, value):
+        warnings.warn('setConfigOption() will go away in a future version of Yum.\n'
+                'Please set option values as attributes or using setattr().',
+                DeprecationWarning)
+        if hasattr(self, option):
+            setattr(self, option, value)
+        else:
+            raise ConfigError, 'No such option %s' % option
+
+
+def read_config(config_file, BaseConfigDerived):
+    confparser = ConfigParser()
+    opts = BaseConfigDerived()
+    if os.path.exists(config_file):
+        try:
+            confparser.read(config_file)
+        except ParsingError, e:
+            print >> sys.stderr, "Error reading config file: %s" % e
+            sys.exit(1)
+    opts.populate(confparser, 'main')
+    return opts
diff --git a/func/forkbomb.py b/func/forkbomb.py
new file mode 100644 (file)
index 0000000..3dfa6f2
--- /dev/null
@@ -0,0 +1,153 @@
+# forkbomb is a module that partitions arbitrary workloads
+# among N seperate forks, for a configurable N, and
+# collates results upon return, as if it never forked.
+#
+# Copyright 2007, Red Hat, Inc
+# Michael DeHaan <mdehaan@redhat.com>
+#
+# This software may be freely redistributed under the terms of the GNU
+# general public license.
+#
+# 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., 675 Mass Ave, Cambridge, MA 02139, USA.
+
+import os
+import random # for testing only
+import time   # for testing only
+import shelve
+import bsddb
+import sys
+import tempfile
+import fcntl
+import utils
+import xmlrpclib
+
+DEFAULT_FORKS = 4
+DEFAULT_CACHE_DIR = "/var/lib/func"
+
+def __get_storage(dir):
+    """
+    Return a tempfile we can use for storing data.
+    """
+    dir = os.path.expanduser(dir)
+    if not os.path.exists(dir):
+        os.makedirs(dir)
+    return tempfile.mktemp(suffix='', prefix='asynctmp', dir=dir)
+
+def __access_buckets(filename,clear,new_key=None,new_value=None):
+    """
+    Access data in forkbomb cache, potentially clearing or
+    modifying it as required.
+    """
+
+    internal_db = bsddb.btopen(filename, 'c', 0644 )
+    handle = open(filename,"r")
+    fcntl.flock(handle.fileno(), fcntl.LOCK_EX)
+    storage = shelve.BsdDbShelf(internal_db)
+
+    if clear:
+        storage.clear()
+        storage.close()
+        fcntl.flock(handle.fileno(), fcntl.LOCK_UN)
+        return {}
+
+    if not storage.has_key("data"):
+        storage["data"] = {}
+    else: 
+        pass
+
+    if new_key is not None:
+        # bsdb is a bit weird about this
+        newish = storage["data"].copy()
+        newish[new_key] = new_value
+        storage["data"] = newish
+
+    rc = storage["data"].copy()
+    storage.close()
+    fcntl.flock(handle.fileno(), fcntl.LOCK_UN)
+
+    return rc
+
+def __bucketize(pool, slots):
+    """
+    Given a pre-existing list of X number of tasks, partition
+    them into a hash of Y number of slots.
+    """
+    buckets = {}
+    count = 0
+    for key in pool:
+        count = count + 1
+        slot = count % slots
+        if not buckets.has_key(slot):
+            buckets[slot] = [] 
+        buckets[slot].append(key)
+    return buckets
+
+def __with_my_bucket(bucket_number,buckets,what_to_do,filename):
+    """
+    Process all tasks assigned to a given fork, and save
+    them in the shelf.
+    """
+    things_in_my_bucket = buckets[bucket_number]
+    results = {}
+    for thing in things_in_my_bucket:
+        (nkey,nvalue) = what_to_do(bucket_number,buckets,thing)
+        __access_buckets(filename,False,nkey,nvalue)
+
+def __forkbomb(mybucket,buckets,what_to_do,filename):
+    """
+    Recursive function to spawn of a lot of worker forks.
+    """
+    nbuckets = len(buckets)
+    pid = os.fork()
+    if pid != 0:
+        if mybucket < (nbuckets-1):
+            __forkbomb(mybucket+1,buckets,what_to_do,filename)
+        try:
+            os.waitpid(pid,0)
+        except OSError, ose:
+            if ose.errno == 10:
+                pass
+            else:
+                raise ose
+    else:
+        __with_my_bucket(mybucket,buckets,what_to_do,filename)    
+        sys.exit(0)
+
+def __demo(bucket_number, buckets, my_item):
+    """ 
+    This is a demo handler for test purposes.
+    It just multiplies all numbers by 1000, but slowly.
+    """
+    # print ">> I am fork (%s) and I am processing item (%s)" % (bucket_number, my_item)
+    # just to verify forks are not sequential
+    sleep = random.randrange(0,4)
+    time.sleep(sleep)
+    return (my_item, my_item * 1000)
+
+def batch_run(pool,callback,nforks=DEFAULT_FORKS,cachedir=DEFAULT_CACHE_DIR):
+    """
+    Given an array of items (pool), call callback in each one, but divide
+    the workload over nfork forks.  Temporary files used during the
+    operation will be created in cachedir and subsequently deleted.    
+    """
+    if nforks <= 1:
+       # modulus voodoo gets crazy otherwise and bad things happen
+       nforks = 2
+    shelf_file = __get_storage(cachedir)
+    __access_buckets(shelf_file,True,None)
+    buckets = __bucketize(pool, nforks)
+    __forkbomb(1,buckets,callback,shelf_file)
+    rc = __access_buckets(shelf_file,False,None)
+    os.remove(shelf_file)
+    return rc
+
+def __test(nforks=4,sample_size=20):
+    pool = xrange(0,sample_size)
+    print batch_run(pool,__demo,nforks=nforks)
+
+if __name__ == "__main__":
+    __test()
+
+
diff --git a/func/jobthing.py b/func/jobthing.py
new file mode 100644 (file)
index 0000000..67ad1a6
--- /dev/null
@@ -0,0 +1,204 @@
+# jobthing is a module that allows for background execution of a task, and
+# getting status of that task.  The ultimate goal is to allow ajaxyness
+# of GUI apps using Func, and also for extremely long running tasks that
+# we don't want to block on as called by scripts using the FunC API.  The
+# CLI should not use this.
+#
+# Copyright 2007, Red Hat, Inc
+# Michael DeHaan <mdehaan@redhat.com>
+#
+# This software may be freely redistributed under the terms of the GNU
+# general public license.
+#
+# 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., 675 Mass Ave, Cambridge, MA 02139, USA.
+
+import os
+import random # for testing only
+import time   # for testing only
+import shelve
+import bsddb
+import sys
+import tempfile
+import fcntl
+import forkbomb
+import utils
+import traceback
+
+JOB_ID_RUNNING = 0
+JOB_ID_FINISHED = 1
+JOB_ID_LOST_IN_SPACE = 2
+JOB_ID_ASYNC_PARTIAL = 3
+JOB_ID_ASYNC_FINISHED = 4
+
+# how long to retain old job records in the job id database
+RETAIN_INTERVAL = 60 * 60    
+
+# where to store the internal job id database
+CACHE_DIR = "/var/lib/func"
+
+def __update_status(jobid, status, results, clear=False):
+    return __access_status(jobid=jobid, status=status, results=results, write=True)
+
+def __get_status(jobid):
+    return __access_status(jobid=jobid, write=False)
+
+def purge_old_jobs():
+    return __access_status(purge=True)
+
+def __purge_old_jobs(storage):
+    """
+    Deletes jobs older than RETAIN_INTERVAL seconds.  
+    MINOR FIXME: this probably should be a more intelligent algorithm that only
+    deletes jobs if the database is too big and then only the oldest jobs
+    but this will work just as well.
+    """
+    nowtime = time.time()
+    for x in storage.keys():
+        # minion jobs have "-minion" in the job id so disambiguation so we need to remove that
+        jobkey = x.replace("-","").replace("minion","")
+        create_time = float(jobkey)
+        if nowtime - create_time > RETAIN_INTERVAL:
+            del storage[x]
+
+def __access_status(jobid=0, status=0, results=0, clear=False, write=False, purge=False):
+
+    dir = os.path.expanduser(CACHE_DIR)
+    if not os.path.exists(dir):
+        os.makedirs(dir)
+    filename = os.path.join(dir,"status-%s" % os.getuid()) 
+
+    internal_db = bsddb.btopen(filename, 'c', 0644 )
+    handle = open(filename,"r")
+    fcntl.flock(handle.fileno(), fcntl.LOCK_EX)
+    storage = shelve.BsdDbShelf(internal_db)
+
+
+    if clear:
+        storage.clear()
+        storage.close()
+        fcntl.flock(handle.fileno(), fcntl.LOCK_UN)
+        return {}
+    
+    if purge or write:
+        __purge_old_jobs(storage)
+
+    if write:
+        storage[str(jobid)] = (status, results)
+        rc = jobid
+    elif not purge:
+        if storage.has_key(str(jobid)):
+            # tuple of (status, results)
+
+            rc = storage[str(jobid)]
+        else:
+            rc = (JOB_ID_LOST_IN_SPACE, 0)
+    else:
+        rc = 0
+
+    storage.close()
+    fcntl.flock(handle.fileno(), fcntl.LOCK_UN)
+
+    return rc
+
+def batch_run(server, process_server, nforks):
+    """
+    This is the method used by the overlord side usage of jobthing.
+    Minion side usage will use minion_async_run instead.
+
+    Given an array of items (pool), call callback in each one, but divide
+    the workload over nfork forks.  Temporary files used during the
+    operation will be created in cachedir and subsequently deleted.    
+    """
+   
+    job_id = time.time()
+    pid = os.fork()
+    if pid != 0:
+        __update_status(job_id, JOB_ID_RUNNING, -1)
+        return job_id
+    else:
+        # kick off the job
+        __update_status(job_id, JOB_ID_RUNNING,  -1)
+        results = forkbomb.batch_run(server, process_server, nforks)
+        
+        # we now have a list of job id's for each minion, kill the task
+        __update_status(job_id, JOB_ID_ASYNC_PARTIAL, results)
+        sys.exit(0)
+
+def minion_async_run(retriever, method, args):
+    """
+    This is a simpler invocation for minion side async usage.
+    """
+    # to avoid confusion of job id's (we use the same job database)
+    # minion jobs contain the string "minion".  
+
+
+    job_id = "%s-minion" % time.time()
+    pid = os.fork()
+    if pid != 0:
+        __update_status(job_id, JOB_ID_RUNNING, -1)
+        return job_id
+    else:
+        __update_status(job_id, JOB_ID_RUNNING,  -1)
+        try:
+            function_ref = retriever(method)
+            rc = function_ref(*args)
+        except Exception, e:
+            (t, v, tb) = sys.exc_info()
+            rc = utils.nice_exception(t,v,tb)
+
+        __update_status(job_id, JOB_ID_FINISHED, rc)
+        sys.exit(0)
+
+def job_status(jobid, client_class=None):
+    # NOTE: client_class is here to get around some evil circular reference
+    # type stuff.  This is intended to be called by minions (who can leave it None)
+    # or by the Client module code (which does not need to be worried about it).  API
+    # users should not be calling jobthing.py methods directly.
+   
+    got_status = __get_status(jobid)
+
+    # if the status comes back as JOB_ID_ASYNC_PARTIAL what we have is actually a hash
+    # of hostname/minion-jobid pairs.  Instantiate a client handle for each and poll them
+    # for their actual status, filling in only the ones that are actually done.
+
+    (interim_rc, interim_results) = got_status
+
+    if interim_rc == JOB_ID_ASYNC_PARTIAL:
+
+        partial_results = {}
+
+
+        some_missing = False
+        for host in interim_results.keys():
+
+            minion_job = interim_results[host]
+            client = client_class(host, noglobs=True, async=False)
+            minion_result = client.jobs.job_status(minion_job)
+
+            (minion_interim_rc, minion_interim_result) = minion_result
+
+            if minion_interim_rc not in [ JOB_ID_RUNNING ]:
+                if minion_interim_rc in [ JOB_ID_LOST_IN_SPACE ]:
+                    partial_results[host] = [ utils.REMOTE_ERROR, "lost job" ]
+                else:
+                    partial_results[host] = minion_interim_result
+            else: 
+                some_missing = True
+
+        if some_missing:
+            return (JOB_ID_ASYNC_PARTIAL, partial_results)
+        else:
+            return (JOB_ID_ASYNC_FINISHED, partial_results)
+
+    else:
+        return got_status
+   
+    # of job id's on the minion in results.
+
+if __name__ == "__main__":
+    __test()
+
+
diff --git a/func/logger.py b/func/logger.py
new file mode 100755 (executable)
index 0000000..e679f3d
--- /dev/null
@@ -0,0 +1,76 @@
+## func
+##
+## Copyright 2007, Red Hat, Inc
+## See AUTHORS
+##
+## This software may be freely redistributed under the terms of the GNU
+## general public license.
+##
+## 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., 675 Mass Ave, Cambridge, MA 02139, USA.
+##
+##
+
+
+import logging
+from func.config import read_config
+from func.commonconfig import FuncdConfig
+
+
+# from the comments in http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/66531
+class Singleton(object):
+    def __new__(type, *args, **kwargs):
+        if not '_the_instance' in type.__dict__:
+            type._the_instance = object.__new__(type, *args, **kwargs)
+        return type._the_instance
+
+# logging is weird, we don't want to setup multiple handlers
+# so make sure we do that mess only once
+
+class Logger(Singleton):
+    _no_handlers = True
+
+    def __init__(self, logfilepath ="/var/log/func/func.log"):
+        config_file = '/etc/func/minion.conf'
+        self.config = read_config(config_file, FuncdConfig)    
+        self.loglevel = logging._levelNames[self.config.log_level]
+        self._setup_logging()
+        if self._no_handlers:
+            self._setup_handlers(logfilepath=logfilepath)
+        
+    def _setup_logging(self):
+        self.logger = logging.getLogger("svc")
+
+    def _setup_handlers(self, logfilepath="/var/log/func/func.log"):
+        handler = logging.FileHandler(logfilepath, "a")
+        self.logger.setLevel(self.loglevel)
+        formatter = logging.Formatter("%(asctime)s - %(levelname)s - %(message)s")
+        handler.setFormatter(formatter)
+        self.logger.addHandler(handler)
+        self._no_handlers = False
+
+
+class AuditLogger(Singleton):
+    _no_handlers = True
+    def __init__(self, logfilepath = "/var/log/func/audit.log"):
+        self.loglevel = logging.INFO
+        self._setup_logging()
+        if self._no_handlers:
+            self._setup_handlers(logfilepath=logfilepath)
+
+    def log_call(self, ip, CN, cert_hash, method, params):
+        # square away a good parseable format at some point -akl
+        self.logger.info("%s %s %s %s called with %s" % (ip, CN, cert_hash, method, params))
+
+
+    def _setup_logging(self):
+        self.logger = logging.getLogger("audit")
+
+    def _setup_handlers(self, logfilepath="/var/log/func/audit.log"):
+        handler = logging.FileHandler(logfilepath, "a")
+        self.logger.setLevel(self.loglevel)
+        formatter = logging.Formatter("%(asctime)s - %(message)s")
+        handler.setFormatter(formatter)
+        self.logger.addHandler(handler)
+        self._no_handlers = False
diff --git a/func/minion/AuthedXMLRPCServer.py b/func/minion/AuthedXMLRPCServer.py
new file mode 100644 (file)
index 0000000..0ec9ce0
--- /dev/null
@@ -0,0 +1,140 @@
+# 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 Library 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+#
+# Copyright 2005 Dan Williams <dcbw@redhat.com> and Red Hat, Inc.
+# Modifications by Seth Vidal - 2007
+
+import sys
+import socket
+import SimpleXMLRPCServer
+from func import SSLCommon
+import OpenSSL
+import SocketServer
+
+
+class AuthedSimpleXMLRPCRequestHandler(SimpleXMLRPCServer.SimpleXMLRPCRequestHandler):
+
+    # For some reason, httplib closes the connection right after headers
+    # have been sent if the connection is _not_ HTTP/1.1, which results in
+    # a "Bad file descriptor" error when the client tries to read from the socket
+    protocol_version = "HTTP/1.1"
+
+    def setup(self):
+        """
+        We need to use socket._fileobject Because SSL.Connection
+        doesn't have a 'dup'. Not exactly sure WHY this is, but
+        this is backed up by comments in socket.py and SSL/connection.c
+        """
+        self.connection = self.request # for doPOST
+        self.rfile = socket._fileobject(self.request, "rb", self.rbufsize)
+        self.wfile = socket._fileobject(self.request, "wb", self.wbufsize)
+
+    def do_POST(self):
+        self.server._this_request = (self.request, self.client_address)
+        try:
+            SimpleXMLRPCServer.SimpleXMLRPCRequestHandler.do_POST(self)
+        except socket.timeout:
+            pass
+        except (socket.error, OpenSSL.SSL.SysCallError), e:
+            print "Error (%s): socket error - '%s'" % (self.client_address, e)
+
+
+class BaseAuthedXMLRPCServer(SocketServer.ThreadingMixIn):
+    def __init__(self, address, authinfo_callback=None):
+        self.allow_reuse_address = 1
+        self.logRequests = 1
+        self.authinfo_callback = authinfo_callback
+
+        self.funcs = {}
+        self.instance = None
+
+    def get_authinfo(self, request, client_address):
+        print 'down here'
+        if self.authinfo_callback:
+            return self.authinfo_callback(request, client_address)
+        return None
+
+
+class AuthedSSLXMLRPCServer(BaseAuthedXMLRPCServer, SSLCommon.BaseSSLServer, SimpleXMLRPCServer.SimpleXMLRPCServer):
+    """ Extension to allow more fine-tuned SSL handling """
+
+    def __init__(self, address, pkey, cert, ca_cert, authinfo_callback=None, timeout=None):
+        BaseAuthedXMLRPCServer.__init__(self, address, authinfo_callback)
+        SimpleXMLRPCServer.SimpleXMLRPCServer.__init__(self, address, AuthedSimpleXMLRPCRequestHandler)
+        SSLCommon.BaseSSLServer.__init__(self, address, AuthedSimpleXMLRPCRequestHandler, pkey, cert, ca_cert, timeout=timeout)
+
+
+
+class AuthedXMLRPCServer(BaseAuthedXMLRPCServer, SSLCommon.BaseServer, SimpleXMLRPCServer.SimpleXMLRPCServer):
+
+    def __init__(self, address, authinfo_callback=None):
+        BaseAuthedXMLRPCServer.__init__(self, address, authinfo_callback)
+        SSLCommon.BaseServer.__init__(self, address, AuthedSimpleXMLRPCRequestHandler)
+
+
+###########################################################
+# Testing stuff
+###########################################################
+
+class ReqHandler:
+    def ping(self, callerid, trynum):
+        print 'clearly not'
+        print callerid
+        print trynum
+        return "pong %d / %d" % (callerid, trynum)
+
+class TestServer(AuthedSSLXMLRPCServer):
+    """
+    SSL XMLRPC server that authenticates clients based on their certificate.
+    """
+
+    def __init__(self, address, pkey, cert, ca_cert):
+        AuthedSSLXMLRPCServer.__init__(self, address, pkey, cert, ca_cert, self.auth_cb)
+
+    def _dispatch(self, method, params):
+        if method == 'trait_names' or method == '_getAttributeNames':
+            return dir(self)
+        # if we have _this_request then we get the peer cert from it
+        # handling all the authZ checks in _dispatch() means we don't even call the method
+        # for whatever it wants to do and we have the method name.
+
+        if hasattr(self, '_this_request'):
+            r,a = self._this_request
+            p = r.get_peer_certificate()
+            print dir(p)
+            print p.get_subject()
+        else:
+            print 'no cert'
+
+        return "your mom"
+
+    def auth_cb(self, request, client_address):
+        peer_cert = request.get_peer_certificate()
+        return peer_cert.get_subject().CN
+
+
+if __name__ == '__main__':
+    if len(sys.argv) < 4:
+        print "Usage: python AuthdXMLRPCServer.py key cert ca_cert"
+        sys.exit(1)
+
+    pkey = sys.argv[1]
+    cert = sys.argv[2]
+    ca_cert = sys.argv[3]
+
+    print "Starting the server."
+    server = TestServer(('localhost', 51234), pkey, cert, ca_cert)
+    h = ReqHandler()
+    server.register_instance(h)
+    server.serve_forever()
diff --git a/func/minion/Makefile b/func/minion/Makefile
new file mode 100755 (executable)
index 0000000..d630382
--- /dev/null
@@ -0,0 +1,24 @@
+
+
+PYFILES                = $(wildcard *.py)
+PYDIRS = modules
+
+PYCHECKER       = /usr/bin/pychecker
+PYFLAKES       = /usr/bin/pyflakes
+
+clean::
+       @rm -fv *.pyc *~ .*~ *.pyo
+       @find . -name .\#\* -exec rm -fv {} \;
+       @rm -fv *.rpm
+
+
+pychecker::
+       @$(PYCHECKER) $(PYFILES) || exit 0
+
+pyflakes::
+       @$(PYFLAKES) $(PYFILES) || exit 0
+
+pychecker::
+       -for d in $(PYDIRS); do ($(MAKE) -C $$d pychecker ); done   
+pyflakes::
+       -for d in $(PYDIRS); do ($(MAKE) -C $$d pyflakes ); done        
diff --git a/func/minion/__init__.py b/func/minion/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/func/minion/codes.py b/func/minion/codes.py
new file mode 100755 (executable)
index 0000000..a20c95e
--- /dev/null
@@ -0,0 +1,29 @@
+"""
+func
+
+Copyright 2007, Red Hat, Inc
+See AUTHORS
+
+This software may be freely redistributed under the terms of the GNU
+general public license.
+
+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., 675 Mass Ave, Cambridge, MA 02139, USA.
+"""
+
+import exceptions
+
+
+class FuncException(exceptions.Exception):
+    pass
+
+
+class InvalidMethodException(FuncException):
+    pass
+
+
+class AccessToMethodDenied(FuncException):
+    pass
+    
+# FIXME: more sub-exceptions maybe
diff --git a/func/minion/module_loader.py b/func/minion/module_loader.py
new file mode 100755 (executable)
index 0000000..3068ea8
--- /dev/null
@@ -0,0 +1,118 @@
+## func
+##
+## Copyright 2007, Red Hat, Inc
+## See AUTHORS
+##
+## This software may be freely redistributed under the terms of the GNU
+## general public license.
+##
+## 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., 675 Mass Ave, Cambridge, MA 02139, USA.
+##
+##
+
+
+import distutils.sysconfig
+import os
+import sys
+from gettext import gettext
+_ = gettext
+
+from func import logger
+logger = logger.Logger().logger
+
+from inspect import isclass
+from func.minion.modules import func_module
+
+def module_walker(topdir):
+    module_files = []
+    for root, dirs, files in os.walk(topdir):
+        # we should get here for each subdir
+        for filename in files:
+            # ASSUMPTION: all module files will end with .py, .pyc, .pyo
+            if filename[-3:] == ".py" or filename[-4:] == ".pyc" or filename[-4:] == ".pyo":
+                # the normpath is important, since we eventually replace /'s with .'s
+                # in the module name, and foo..bar doesnt work -akl
+                module_files.append(os.path.normpath("%s/%s" % (root, filename)))
+
+
+    return module_files
+
+def load_modules(blacklist=None):
+
+    module_file_path="%s/func/minion/modules/" % distutils.sysconfig.get_python_lib()
+    mod_path="%s/func/minion/"  % distutils.sysconfig.get_python_lib()
+
+    sys.path.insert(0, mod_path)
+    mods = {}
+    bad_mods = {}
+
+    filenames = module_walker(module_file_path)
+
+    # FIXME: this is probably more complicated than it needs to be -akl
+    for fn in filenames:
+        # aka, everything after the module_file_path
+        module_name_part = fn[len(module_file_path):]
+        dirname, basename = os.path.split(module_name_part)
+
+        if basename[:8] == "__init__":
+            modname = dirname
+            dirname = ""
+        elif basename[-3:] == ".py":
+            modname = basename[:-3]
+        elif basename[-4:] in [".pyc", ".pyo"]:
+            modname = basename[:-4]
+
+        pathname = modname
+        if dirname != "":
+            pathname = "%s/%s" % (dirname, modname)
+
+        mod_imp_name = pathname.replace("/", ".")
+
+        if mods.has_key(mod_imp_name):
+            # If we've already imported mod_imp_name, don't import it again
+            continue
+
+        # ignore modules that we've already determined aren't valid modules
+        if bad_mods.has_key(mod_imp_name):
+            continue
+
+        try:
+            # Auto-detect and load all FuncModules
+            blip =  __import__("modules.%s" % ( mod_imp_name), globals(), locals(), [mod_imp_name])
+            for obj in dir(blip):
+                attr = getattr(blip, obj)
+                if isclass(attr) and issubclass(attr, func_module.FuncModule):
+                    logger.debug("Loading %s module" % attr)
+                    mods[mod_imp_name] = attr()
+
+        except ImportError, e:
+            # A module that raises an ImportError is (for now) simply not loaded.
+            errmsg = _("Could not load %s module: %s")
+            logger.warning(errmsg % (mod_imp_name, e))
+            bad_mods[mod_imp_name] = True
+            continue
+        except:
+            errmsg = _("Could not load %s module")
+            logger.warning(errmsg % (mod_imp_name))
+            bad_mods[mod_imp_name] = True
+            continue
+
+    return mods
+
+
+if __name__ == "__main__":
+
+    module_file_path = "/usr/lib/python2.5/site-packages/func/minion/modules/"
+    bar = module_walker(module_file_path)
+    print bar
+    for f in bar:
+        print f
+        print os.path.basename(f)
+        print os.path.split(f)
+        g = f[len(module_file_path):]
+        print g
+        print os.path.split(g)
+
+    print load_modules()
diff --git a/func/minion/modules/Makefile b/func/minion/modules/Makefile
new file mode 100755 (executable)
index 0000000..f2bc6c4
--- /dev/null
@@ -0,0 +1,18 @@
+
+
+PYFILES                = $(wildcard *.py)
+
+PYCHECKER       = /usr/bin/pychecker
+PYFLAKES       = /usr/bin/pyflakes
+
+clean::
+       @rm -fv *.pyc *~ .*~ *.pyo
+       @find . -name .\#\* -exec rm -fv {} \;
+       @rm -fv *.rpm
+
+
+pychecker::
+       @$(PYCHECKER) $(PYFILES) || exit 0
+
+pyflakes::
+       @$(PYFLAKES) $(PYFILES) || exit 0
diff --git a/func/minion/modules/__init__.py b/func/minion/modules/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/func/minion/modules/certmaster.py b/func/minion/modules/certmaster.py
new file mode 100644 (file)
index 0000000..9ca484f
--- /dev/null
@@ -0,0 +1,65 @@
+## -*- coding: utf-8 -*-
+##
+## Process lister (control TBA)
+##
+## Copyright 2008, Red Hat, Inc
+## Michael DeHaan <mdehaan@redhat.com>
+##
+## This software may be freely redistributed under the terms of the GNU
+## general public license.
+##
+## 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., 675 Mass Ave, Cambridge, MA 02139, USA.
+##
+
+# other modules
+import sub_process
+import codes
+
+# our modules
+import func_module
+from func import certmaster as certmaster
+
+# =================================
+
+class CertMasterModule(func_module.FuncModule):
+
+    version = "0.0.1"
+    api_version = "0.0.1"
+    description = "Administers certs on an overlord."
+
+    def get_hosts_to_sign(self, list_of_hosts):
+        """
+        ...
+        """
+        list_of_hosts = self.__listify(list_of_hosts)
+        cm = certmaster.CertMaster()
+        return cm.get_csrs_waiting()
+
+    def sign_hosts(self, list_of_hosts):
+        """
+        ...
+        """
+        list_of_hosts = self.__listify(list_of_hosts)
+        cm = certmaster.CertMaster()
+        for x in list_of_hosts:
+           cm.sign_this_csr(x)
+        return True
+
+    def cleanup_hosts(self, list_of_hosts):
+        """
+        ...
+        """
+        list_of_hosts = self.__listify(list_of_hosts)
+        cm = certmaster.CertMaster()
+        for x in list_of_hosts:
+           cm.remove_this_cert(x)
+        return True
+
+    def __listify(self, list_of_hosts):
+        if type(list_of_hosts) is type([]):
+            return list_of_hosts
+        else:
+            return [ list_of_hosts ]    
+
diff --git a/func/minion/modules/command.py b/func/minion/modules/command.py
new file mode 100644 (file)
index 0000000..cc463cf
--- /dev/null
@@ -0,0 +1,44 @@
+# Copyright 2007, Red Hat, Inc
+# James Bowes <jbowes@redhat.com>
+# Steve 'Ashcrow' Milner <smilner@redhat.com>
+#
+# This software may be freely redistributed under the terms of the GNU
+# general public license.
+#
+# 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., 675 Mass Ave, Cambridge, MA 02139, USA.
+
+"""
+Abitrary command execution module for func.
+"""
+
+import func_module
+import sub_process
+
+class Command(func_module.FuncModule):
+
+    version = "0.0.1"
+    api_version = "0.0.1"
+    description = "Works with shell commands."
+
+    def run(self, command):
+        """
+        Runs a command, returning the return code, stdout, and stderr as a tuple.
+        NOT FOR USE WITH INTERACTIVE COMMANDS.
+        """
+
+        cmdref = sub_process.Popen(command.split(), stdout=sub_process.PIPE,
+                                   stderr=sub_process.PIPE, shell=False)
+        data = cmdref.communicate()
+        return (cmdref.returncode, data[0], data[1])
+
+    def exists(self, command):
+        """
+        Checks to see if a command exists on the target system(s).
+        """
+        import os
+
+        if os.access(command, os.X_OK):
+            return True
+        return False
diff --git a/func/minion/modules/copyfile.py b/func/minion/modules/copyfile.py
new file mode 100644 (file)
index 0000000..150af88
--- /dev/null
@@ -0,0 +1,109 @@
+# Copyright 2007, Red Hat, Inc
+# seth vidal
+#
+# This software may be freely redistributed under the terms of the GNU
+# general public license.
+#
+# 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., 675 Mass Ave, Cambridge, MA 02139, USA.
+
+
+import sha
+import os
+import time
+import shutil
+
+import func_module
+
+
+class CopyFile(func_module.FuncModule):
+
+    version = "0.0.1"
+    api_version = "0.0.2"
+    description = "Allows for smart copying of a file."
+
+    def _checksum_blob(self, blob):
+        thissum = sha.new()
+        thissum.update(blob)
+        return thissum.hexdigest()
+                       
+    def checksum(self, thing):
+
+        CHUNK=2**16
+        thissum = sha.new()
+        if os.path.exists(thing):
+            fo = open(thing, 'r', CHUNK)
+            chunk = fo.read
+            while chunk:
+                chunk = fo.read(CHUNK)
+                thissum.update(chunk)
+            fo.close()
+            del fo
+        else:
+            # assuming it's a string of some kind
+            thissum.update(thing)
+
+        return thissum.hexdigest()
+
+
+    def copyfile(self, filepath, filebuf, mode=0644, uid=0, gid=0, force=None):
+        # -1 = problem file was not copied
+        # 1 =  file was copied
+        # 0 = file was not copied b/c file is unchanged
+
+
+        # we should probably verify mode,uid,gid are valid as well
+
+        dirpath = os.path.dirname(filepath)
+        if not os.path.exists(dirpath):
+            os.makedirs(dirpath)
+
+        remote_sum = self._checksum_blob(filebuf.data)
+        local_sum = 0
+        if os.path.exists(filepath):
+            local_sum = self.checksum(filepath)
+
+        if remote_sum != local_sum or force is not None:
+            # back up the localone
+            if os.path.exists(filepath):
+                if not self._backuplocal(filepath):
+                    return -1
+
+            # do the new write
+            try:
+                fo = open(filepath, 'w')
+                fo.write(filebuf.data)
+                fo.close()
+                del fo
+            except (IOError, OSError), e:
+                # XXX logger output here
+                return -1
+        else:
+            return 0
+
+        # hmm, need to figure out proper exceptions -akl
+        try:
+            # we could intify the mode here if it's a string
+            os.chmod(filepath, mode)
+            os.chown(filepath, uid, gid)
+        except (IOError, OSError), e:
+            return -1
+
+        return 1
+
+    def _backuplocal(self, fn):
+        """
+        make a date-marked backup of the specified file,
+        return True or False on success or failure
+        """
+        # backups named basename-YYYY-MM-DD@HH:MM~
+        ext = time.strftime("%Y-%m-%d@%H:%M~", time.localtime(time.time()))
+        backupdest = '%s.%s' % (fn, ext)
+
+        try:
+            shutil.copy2(fn, backupdest)
+        except shutil.Error, e:
+            #XXX logger output here
+            return False
+        return True
diff --git a/func/minion/modules/filetracker.py b/func/minion/modules/filetracker.py
new file mode 100644 (file)
index 0000000..f5f9dbb
--- /dev/null
@@ -0,0 +1,192 @@
+## func
+##
+## filetracker
+##  maintains a manifest of files of which to keep track
+##  provides file meta-data (and optionally full data) to func-inventory
+##
+## (C) Vito Laurenza <vitolaurenza@gmail.com>
+## + Michael DeHaan <mdehaan@redhat.com>
+##
+## This software may be freely redistributed under the terms of the GNU
+## general public license.
+##
+## 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., 675 Mass Ave, Cambridge, MA 02139, USA.
+##
+
+# func modules
+import func_module
+
+# other modules
+from stat import *
+import glob
+import os
+import md5
+
+# defaults
+CONFIG_FILE='/etc/func/modules/filetracker.conf'
+
+class FileTracker(func_module.FuncModule):
+
+    version = "0.0.1"
+    api_version = "0.0.1"
+    description = "Maintains a manifest of files to keep track of."
+
+    def __load(self):
+        """
+        Parse file and return data structure.
+        """
+
+        filehash = {}
+        if os.path.exists(CONFIG_FILE):
+            config = open(CONFIG_FILE, "r")
+            data   = config.read()
+            lines  = data.split("\n")
+            for line in lines:
+                tokens = line.split(None)
+                if len(tokens) < 2:
+                    continue
+                scan_mode = tokens[0]
+                path = " ".join(tokens[1:])
+                if str(scan_mode).lower() == "0":
+                    scan_mode = 0
+                else:
+                    scan_mode = 1
+                filehash[path] = scan_mode
+        return filehash
+
+    #==========================================================
+
+    def __save(self, filehash):
+        """
+        Write data structure to file.
+        """ 
+
+        config = open(CONFIG_FILE, "w+")
+        for (path, scan_mode) in filehash.iteritems():
+            config.write("%s     %s\n" % (scan_mode, path))
+        config.close()
+
+    #==========================================================
+               
+    def track(self, file_name, full_scan=0):
+        """
+        Adds files to keep track of.
+        full_scan implies tracking the full contents of the file, defaults to off
+        """
+
+        filehash = self.__load()
+        filehash[file_name] = full_scan
+        self.__save(filehash)
+        return 1
+
+    #==========================================================
+
+    def untrack(self, file_name):
+        """
+        Stop keeping track of a file.
+        This routine is tolerant of most errors since we're forgetting about the file anyway.
+        """
+
+        filehash = self.__load()
+        if file_name in filehash.keys():
+            del filehash[file_name]
+        self.__save(filehash)
+        return 1
+
+    #==========================================================
+
+    def inventory(self, flatten=1, checksum_enabled=1):
+        """
+        Returns information on all tracked files
+        By default, 'flatten' is passed in as True, which makes printouts very clean in diffs
+        for use by func-inventory.  If you are writting another software application, using flatten=False will
+        prevent the need to parse the returns.
+        """
+        # XMLRPC feeds us strings from the CLI when it shouldn't
+        flatten = int(flatten)
+        checksum_enabled = int(checksum_enabled)
+
+        filehash = self.__load()
+
+        # we'll either return a very flat string (for clean diffs)
+        # or a data structure
+        if flatten:
+            results = ""
+        else:
+            results = []
+
+        for (file_name, scan_type) in filehash.iteritems():
+
+            if not os.path.exists(file_name):
+                if flatten:
+                    results = results + "%s: does not exist\n" % file_name
+                else:
+                    results.append("%s: does not exist\n" % file_name)
+                continue
+
+            this_result = []
+
+            # ----- always process metadata
+            filestat = os.stat(file_name)
+            mode = filestat[ST_MODE]
+            mtime = filestat[ST_MTIME]
+            uid = filestat[ST_UID]
+            gid = filestat[ST_GID]
+            if not os.path.isdir(file_name) and checksum_enabled:
+                sum_handle = open(file_name)
+                hash = self.__sumfile(sum_handle)
+                sum_handle.close()
+            else:
+                hash = "N/A"
+
+            # ------ what we return depends on flatten
+            if flatten:
+                this_result = "%s:  mode=%s  mtime=%s  uid=%s  gid=%s  md5sum=%s\n" % (file_name,mode,mtime,uid,gid,hash) 
+            else:
+                this_result = [file_name,mode,mtime,uid,gid,hash]
+
+            # ------ add on file data only if requested
+            if scan_type != 0 and os.path.isfile(file_name):
+                tracked_file = open(file_name)
+                data = tracked_file.read()
+                if flatten:
+                    this_result = this_result + "*** DATA ***\n" + data + "\n*** END DATA ***\n\n"
+                else:
+                    this_result.append(data)
+                tracked_file.close()
+           
+            if os.path.isdir(file_name):
+                if not file_name.endswith("/"):
+                    file_name = file_name + "/"
+                files = glob.glob(file_name + "*") 
+                if flatten:
+                    this_result = this_result + "*** FILES ***\n" + "\n".join(files) + "\n*** END FILES ***\n\n"
+                else:
+                    this_result.append({"files" : files})
+
+            if flatten:
+                results = results + "\n" + this_result
+            else:
+                results.append(this_result)
+
+
+        return results
+
+    #==========================================================
+
+    def __sumfile(self, fobj):
+        """
+        Returns an md5 hash for an object with read() method.
+        credit: http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/266486
+        """
+
+        m = md5.new()
+        while True:
+            d = fobj.read(8096)
+            if not d:
+                break
+            m.update(d)
+        return m.hexdigest()
diff --git a/func/minion/modules/func_module.py b/func/minion/modules/func_module.py
new file mode 100644 (file)
index 0000000..7d476dc
--- /dev/null
@@ -0,0 +1,76 @@
+##
+## Copyright 2007, Red Hat, Inc
+## see AUTHORS
+##
+## This software may be freely redistributed under the terms of the GNU
+## general public license.
+##
+## 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., 675 Mass Ave, Cambridge, MA 02139, USA.
+##
+
+import inspect
+
+from func import logger
+from func.config import read_config
+from func.commonconfig import FuncdConfig
+
+
+class FuncModule(object):
+
+    # the version is meant to
+    version = "0.0.0"
+    api_version = "0.0.0"
+    description = "No Description provided"
+
+    def __init__(self):
+
+        config_file = '/etc/func/minion.conf'
+        self.config = read_config(config_file, FuncdConfig)
+        self.__init_log()
+        self.__base_methods = {
+            # __'s so we don't clobber useful names
+            "module_version" : self.__module_version,
+            "module_api_version" : self.__module_api_version,
+            "module_description" : self.__module_description,
+            "list_methods"       : self.__list_methods
+        }
+
+    def __init_log(self):
+        log = logger.Logger()
+        self.logger = log.logger
+
+    def register_rpc(self, handlers, module_name):
+        # add the internal methods, note that this means they
+        # can get clobbbered by subclass versions
+        for meth in self.__base_methods:
+            handlers["%s.%s" % (module_name, meth)] = self.__base_methods[meth]
+
+        # register our module's handlers
+        for name, handler in self.__list_handlers().items():
+            handlers["%s.%s" % (module_name, name)] = handler
+
+    def __list_handlers(self):
+        """ Return a dict of { handler_name, method, ... }.
+        All methods that do not being with an underscore will be exposed.
+        We also make sure to not expose our register_rpc method.
+        """
+        handlers = {}
+        for attr in dir(self):
+            if inspect.ismethod(getattr(self, attr)) and attr[0] != '_' and \
+                    attr != 'register_rpc':
+                handlers[attr] = getattr(self, attr)
+        return handlers
+
+    def __list_methods(self):
+        return self.__list_handlers().keys() + self.__base_methods.keys()
+
+    def __module_version(self):
+        return self.version
+
+    def __module_api_version(self):
+        return self.api_version
+
+    def __module_description(self):
+        return self.description
diff --git a/func/minion/modules/func_module.py.orig b/func/minion/modules/func_module.py.orig
new file mode 100644 (file)
index 0000000..c911b91
--- /dev/null
@@ -0,0 +1,65 @@
+##
+## Copyright 2007, Red Hat, Inc
+## see AUTHORS
+##
+## This software may be freely redistributed under the terms of the GNU
+## general public license.
+##
+## 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., 675 Mass Ave, Cambridge, MA 02139, USA.
+##
+
+import inspect
+
+from func import logger
+from func.config import read_config
+from func.commonconfig import FuncdConfig
+
+
+class FuncModule(object):
+
+    # the version is meant to
+    version = "0.0.0"
+    api_version = "0.0.0"
+    description = "No Description provided"
+
+    def __init__(self):
+
+        config_file = '/etc/func/minion.conf'
+        self.config = read_config(config_file, FuncdConfig)
+        self.__init_log()
+        self.__base_methods = {
+            # __'s so we don't clobber useful names
+            "module_version" : self.__module_version,
+            "module_api_version" : self.__module_api_version,
+            "module_description" : self.__module_description,
+            "list_methods"       : self.__list_methods
+        }
+
+    def __init_log(self):
+        log = logger.Logger()
+        self.logger = log.logger
+
+    def register_rpc(self, handlers, module_name):
+        # add the internal methods, note that this means they
+        # can get clobbbered by subclass versions
+        for meth in self.__base_methods:
+            handlers["%s.%s" % (module_name, meth)] = self.__base_methods[meth]
+
+        # register all methods that don't start with an underscore
+        for attr in dir(self):
+            if inspect.ismethod(getattr(self, attr)) and attr[0] != '_':
+                handlers["%s.%s" % (module_name, attr)] = getattr(self, attr)
+
+    def __list_methods(self):
+        return self.methods.keys() + self.__base_methods.keys()
+
+    def __module_version(self):
+        return self.version
+
+    def __module_api_version(self):
+        return self.api_version
+
+    def __module_description(self):
+        return self.description
diff --git a/func/minion/modules/hardware.py b/func/minion/modules/hardware.py
new file mode 100644 (file)
index 0000000..46b1821
--- /dev/null
@@ -0,0 +1,130 @@
+##
+## Hardware profiler plugin
+## requires the "smolt" client package be installed
+## but also relies on lspci for some things
+##
+## Copyright 2007, Red Hat, Inc
+## Michael DeHaan <mdehaan@redhat.com>
+##
+## This software may be freely redistributed under the terms of the GNU
+## general public license.
+##
+## 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., 675 Mass Ave, Cambridge, MA 02139, USA.
+##
+
+
+# other modules
+import sys
+
+# our modules
+import sub_process
+import func_module
+
+# =================================
+
+class HardwareModule(func_module.FuncModule):
+
+    version = "0.0.1"
+    api_version = "0.0.1"
+    description = "Hardware profiler."
+
+    def hal_info(self):
+        """
+        Returns the output of lshal, but split up into seperate devices
+        for easier parsing.  Each device is a entry in the return hash.
+        """
+
+        cmd = sub_process.Popen(["/usr/bin/lshal"],shell=False,stdout=sub_process.PIPE)
+        data = cmd.communicate()[0]
+
+        data = data.split("\n")
+
+        results = {}
+        current = ""
+        label = data[0]
+        for d in data:
+            if d == '':
+                results[label] = current
+                current = ""
+                label = ""
+            else:
+                if label == "":
+                    label = d
+                current = current + d
+
+        return results
+
+    def inventory(self):
+        data = hw_info(with_devices=True)
+        # remove bogomips because it keeps changing for laptops
+        # and makes inventory tracking noisy
+        if data.has_key("bogomips"):
+            del data["bogomips"]
+        return data
+
+    def info(self,with_devices=True):
+        """
+        Returns a struct of hardware information.  By default, this pulls down
+        all of the devices.  If you don't care about them, set with_devices to
+        False.
+        """
+        return hw_info(with_devices)
+
+# =================================
+
+def hw_info(with_devices=True):
+
+    # this may fail if smolt is not installed.  That's ok.  hal_info will
+    # still work.
+
+    # hack: smolt is not installed in site-packages
+    sys.path.append("/usr/share/smolt/client")
+    import smolt
+
+    hardware = smolt.Hardware()
+    host = hardware.host
+
+    # NOTE: casting is needed because these are DBusStrings, not real strings
+    data = {
+        'os'              : str(host.os),
+        'defaultRunlevel' : str(host.defaultRunlevel),
+        'bogomips'        : str(host.bogomips),
+        'cpuVendor'       : str(host.cpuVendor),
+        'cpuModel'        : str(host.cpuModel),
+        'numCpus'         : str(host.numCpus),
+        'cpuSpeed'        : str(host.cpuSpeed),
+        'systemMemory'    : str(host.systemMemory),
+        'systemSwap'      : str(host.systemSwap),
+        'kernelVersion'   : str(host.kernelVersion),
+        'language'        : str(host.language),
+        'platform'        : str(host.platform),
+        'systemVendor'    : str(host.systemVendor),
+        'systemModel'     : str(host.systemModel),
+        'formfactor'      : str(host.formfactor),
+        'selinux_enabled' : str(host.selinux_enabled),
+        'selinux_enforce' : str(host.selinux_enforce)
+    }
+
+    # if no hardware info requested, just return the above bits
+    if not with_devices:
+        return data
+
+    collection = data["devices"] = []
+
+    for item in hardware.deviceIter():
+
+        (VendorID,DeviceID,SubsysVendorID,SubsysDeviceID,Bus,Driver,Type,Description) = item
+
+        collection.append({
+            "VendorID"       : str(VendorID),
+            "DeviceID"       : str(DeviceID),
+            "SubsysVendorID" : str(SubsysVendorID),
+            "Bus"            : str(Bus),
+            "Driver"         : str(Driver),
+            "Type"           : str(Type),
+            "Description"    : str(Description)
+        })
+
+    return data
diff --git a/func/minion/modules/jobs.py b/func/minion/modules/jobs.py
new file mode 100644 (file)
index 0000000..69fb75f
--- /dev/null
@@ -0,0 +1,36 @@
+## (Largely internal) module for access to asynchoronously dispatched
+## module job ID's.  The Func Client() module wraps most of this usage
+## so it's not entirely relevant to folks using the CLI or Func API
+## directly.
+##
+## Copyright 2008, Red Hat, Inc
+## Michael DeHaan <mdehaan@redhat.com>
+##
+## This software may be freely redistributed under the terms of the GNU
+## general public license.
+##
+## 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., 675 Mass Ave, Cambridge, MA 02139, USA.
+##
+
+import codes
+from func import jobthing
+import func_module
+
+# =================================
+
+class JobsModule(func_module.FuncModule):
+
+    version = "0.0.1"
+    api_version = "0.0.1"
+    description = "Internal module for tracking background minion tasks."
+
+    def job_status(self, job_id):
+        """
+        Returns job status in the form of (status, datastruct).
+        Datastruct is undefined for unfinished jobs.  See jobthing.py and
+        Wiki details on async invocation for more information.
+        """
+        return jobthing.job_status(job_id)
+
diff --git a/func/minion/modules/mount.py b/func/minion/modules/mount.py
new file mode 100644 (file)
index 0000000..0db914f
--- /dev/null
@@ -0,0 +1,84 @@
+##
+## Mount manager
+##
+## Copyright 2007, Red Hat, Inc
+## John Eckersberg <jeckersb@redhat.com>
+##
+## This software may be freely redistributed under the terms of the GNU
+## general public license.
+##
+## 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., 675 Mass Ave, Cambridge, MA 02139, USA.
+##
+
+import sub_process, os
+import func_module
+
+
+class MountModule(func_module.FuncModule):
+
+    version = "0.0.1"
+    api_version = "0.0.1"
+    description = "Mounting, unmounting and getting information on mounted filesystems."
+
+    def list(self):
+        cmd = sub_process.Popen(["/bin/cat", "/proc/mounts"], executable="/bin/cat", stdout=sub_process.PIPE, shell=False)
+        data = cmd.communicate()[0]
+        
+        mounts = []
+        lines = [l for l in data.split("\n") if l] #why must you append blank crap?
+
+        for line in lines:
+            curmount = {}
+            tokens = line.split()
+            curmount['device'] = tokens[0]
+            curmount['dir'] = tokens[1]
+            curmount['type'] = tokens[2]
+            curmount['options'] = tokens[3]
+            mounts.append(curmount)
+
+        return mounts
+
+    def mount(self, device, dir, type="auto", options=None, createdir=False):
+        cmdline = ["/bin/mount", "-t", type]
+        if options: 
+            cmdline.append("-o")
+            cmdline.append(options)
+        cmdline.append(device)
+        cmdline.append(dir)
+        if createdir:
+            try:
+                os.makedirs(dir)
+            except:
+                return False
+        cmd = sub_process.Popen(cmdline, executable="/bin/mount", stdout=sub_process.PIPE, shell=False)
+        if cmd.wait() == 0:
+            return True
+        else:
+            return False
+        
+    def umount(self, dir, killall=False, force=False, lazy=False):
+        # succeed if its not mounted
+        if not os.path.ismount(dir):
+            return True
+
+        if killall:
+            cmd = sub_process.Popen(["/sbin/fuser", "-mk", dir], executable="/sbin/fuser", stdout=sub_process.PIPE, shell=False)
+            cmd.wait()
+
+        cmdline = ["/bin/umount"]
+        if force:
+            cmdline.append("-f")
+        if lazy:
+            cmdline.append("-l")
+        cmdline.append(dir)
+
+        cmd = sub_process.Popen(cmdline, executable="/bin/umount", stdout=sub_process.PIPE, shell=False)
+        if cmd.wait() == 0:
+            return True
+        else:
+            return False
+
+    def inventory(self, flatten=True):
+        return self.list()
diff --git a/func/minion/modules/nagios-check.py b/func/minion/modules/nagios-check.py
new file mode 100644 (file)
index 0000000..f1c0714
--- /dev/null
@@ -0,0 +1,34 @@
+# Copyright 2007, Red Hat, Inc
+# James Bowes <jbowes@redhat.com>
+# Seth Vidal modified command.py to be nagios-check.py
+#
+# This software may be freely redistributed under the terms of the GNU
+# general public license.
+#
+# 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., 675 Mass Ave, Cambridge, MA 02139, USA.
+
+"""
+Abitrary command execution module for func.
+"""
+
+import func_module
+import sub_process
+
+class Nagios(func_module.FuncModule):
+
+    version = "0.0.1"
+    api_version = "0.0.1"
+    description = "Runs nagios checks."
+
+    def run(self, check_command):
+        """
+        Runs a nagios check returning the return code, stdout, and stderr as a tuple.
+        """
+        nagios_path='/usr/lib/nagios/plugins'
+        command = '%s/%s' % (nagios_path, check_command)
+        
+        cmdref = sub_process.Popen(command.split(),stdout=sub_process.PIPE,stderr=sub_process.PIPE, shell=False)
+        data = cmdref.communicate()
+        return (cmdref.returncode, data[0], data[1])
diff --git a/func/minion/modules/netapp/README b/func/minion/modules/netapp/README
new file mode 100644 (file)
index 0000000..5ecb205
--- /dev/null
@@ -0,0 +1,8 @@
+This module is meant to be installed on a minion which is configured
+as an admin host for one or more NetApp filers.  Since we can't get
+our funcy awesomeness on the actual filer the admin host will have to do.
+
+Requirements:
+
+- passphraseless ssh key access from root on the netapp admin minion
+  to root on the target filer
diff --git a/func/minion/modules/netapp/TODO b/func/minion/modules/netapp/TODO
new file mode 100644 (file)
index 0000000..25d914c
--- /dev/null
@@ -0,0 +1,5 @@
+Wrap every possible NetApp command :)
+
+I'm only going to do the ones that are important to me.  If you have
+some that are important to you, feel free to submit patches to
+func-list@redhat.com and harness the power of open source!
diff --git a/func/minion/modules/netapp/__init__.py b/func/minion/modules/netapp/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/func/minion/modules/netapp/common.py b/func/minion/modules/netapp/common.py
new file mode 100644 (file)
index 0000000..979c95c
--- /dev/null
@@ -0,0 +1,49 @@
+##
+## NetApp Filer 'common' Module
+##
+## Copyright 2008, Red Hat, Inc
+## John Eckersberg <jeckersb@redhat.com>
+##
+## This software may be freely redistributed under the terms of the GNU
+## general public license.
+##
+## 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., 675 Mass Ave, Cambridge, MA 02139, USA.
+##
+
+import re
+import sub_process 
+
+SSH = '/usr/bin/ssh'
+SSH_USER = 'root'
+SSH_OPTS = '-o forwardagent=no'
+class GenericSSHError(Exception): pass
+class NetappCommandError(Exception): pass
+
+def ssh(host, cmdargs, input=None, user=SSH_USER):
+    cmdline = [SSH, SSH_OPTS, "%s@%s" % (user, host)]
+    cmdline.extend(cmdargs)
+
+    cmd = sub_process.Popen(cmdline,
+                           executable=SSH,
+                           stdin=sub_process.PIPE,
+                           stdout=sub_process.PIPE, 
+                           stderr=sub_process.PIPE,
+                           shell=False)
+
+    (out, err) = cmd.communicate(input)
+
+    if cmd.wait() != 0:
+        raise GenericSSHError, err
+    else:
+        return out + err
+
+def check_output(regex, output):
+    #strip newlines
+    output = output.replace('\n', ' ')
+    if re.search(regex, output):
+        return True
+    else:
+        raise NetappCommandError, output
+
diff --git a/func/minion/modules/netapp/snap.py b/func/minion/modules/netapp/snap.py
new file mode 100644 (file)
index 0000000..8f3f209
--- /dev/null
@@ -0,0 +1,49 @@
+##
+## NetApp Filer 'snap' Module
+##
+## Copyright 2008, Red Hat, Inc
+## John Eckersberg <jeckersb@redhat.com>
+##
+## This software may be freely redistributed under the terms of the GNU
+## general public license.
+##
+## 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., 675 Mass Ave, Cambridge, MA 02139, USA.
+##
+
+import re
+from func.minion.modules import func_module
+from func.minion.modules.netapp.common import *
+
+class Snap(func_module.FuncModule):
+
+    # Update these if need be.
+    version = "0.0.1"
+    api_version = "0.0.1"
+    description = "Interface to the 'snap' command"
+
+    def create(self, filer, vol, snap):
+        """
+        TODO: Document me ...
+        """
+        regex = """creating snapshot..."""
+        cmd_opts = ['snap', 'create', vol, snap]
+        output = ssh(filer, cmd_opts)
+        return check_output(regex, output)
+
+    def delete(self, filer, vol, snap):
+        """
+        TODO: Document me ...
+        """
+        regex = """deleting snapshot..."""
+        cmd_opts = ['snap', 'delete', vol, snap]
+        output = ssh(filer, cmd_opts)
+        return check_output(regex, output)
+    
+    def list(self, filer, vol):
+        """
+        TODO: Document me ...
+        """
+        return True
+
diff --git a/func/minion/modules/netapp/vol/__init__.py b/func/minion/modules/netapp/vol/__init__.py
new file mode 100644 (file)
index 0000000..14ce0ac
--- /dev/null
@@ -0,0 +1,128 @@
+##
+## NetApp Filer 'Vol' Module
+##
+## Copyright 2008, Red Hat, Inc
+## John Eckersberg <jeckersb@redhat.com>
+##
+## This software may be freely redistributed under the terms of the GNU
+## general public license.
+##
+## 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., 675 Mass Ave, Cambridge, MA 02139, USA.
+##
+
+import re
+from func.minion.modules import func_module
+from func.minion.modules.netapp.common import *
+
+class Vol(func_module.FuncModule):
+
+    # Update these if need be.
+    version = "0.0.1"
+    api_version = "0.0.1"
+    description = "Interface to the 'vol' command"
+
+    def create(self, filer, vol, aggr, size):
+        """
+        TODO: Document me ...
+        """
+        regex = """Creation of volume .* has completed."""
+        cmd_opts = ['vol', 'create', vol, aggr, size]
+        output = ssh(filer, cmd_opts)
+        return check_output(regex, output)
+    
+    def destroy(self, filer, vol):
+        """
+        TODO: Document me ...
+        """
+        regex = """Volume .* destroyed."""
+        cmd_opts = ['vol', 'destroy', vol, '-f']
+        output = ssh(filer, cmd_opts)
+        return check_output(regex, output)
+
+    def offline(self, filer, vol):
+        """
+        TODO: Document me ...
+        """
+        regex = """Volume .* is now offline."""
+        cmd_opts = ['vol', 'offline', vol]
+        output = ssh(filer, cmd_opts)
+        return check_output(regex, output)
+
+    def online(self, filer, vol):
+        """
+        TODO: Document me ...
+        """
+        regex = """Volume .* is now online."""
+        cmd_opts = ['vol', 'online', vol]
+        output = ssh(filer, cmd_opts)
+        return check_output(regex, output)
+
+    def status(self, filer, vol=None):
+        """
+        TODO: Document me ...
+        """
+        cmd_opts = ['vol', 'status']
+        output = ssh(filer, cmd_opts)
+
+        output = output.replace(',', ' ')
+        lines = output.split('\n')[1:]
+
+        vols = []
+        current_vol = {}
+        for line in lines:
+            tokens = line.split()
+            if len(tokens) >= 2 and tokens[1] in ('online', 'offline', 'restricted'):
+                if current_vol: vols.append(current_vol)
+                current_vol = {'name': tokens[0], 
+                               'state': tokens[1],
+                               'status': [foo for foo in tokens[2:] if '=' not in foo],
+                               'options': [foo for foo in tokens[2:] if '=' in foo]}
+            else:
+                current_vol['status'].extend([foo for foo in tokens if '=' not in foo])
+                current_vol['options'].extend([foo for foo in tokens if '=' in foo])
+        vols.append(current_vol)
+
+        if vol:
+            try:
+                return [foo for foo in vols if foo['name'] == vol][0]
+            except:
+                raise NetappCommandError, "No such volume: %s" % vol
+        else:
+            return vols
+
+    def size(self, filer, vol, delta=None):
+        """
+        TODO: Document me ...
+        """
+        stat_regex = """vol size: Flexible volume .* has size .*."""
+        resize_regex = """vol size: Flexible volume .* size set to .*."""
+        cmd_opts = ['vol', 'size', vol]
+        
+        if delta:
+            cmd_opts.append(delta)
+            output = ssh(filer, cmd_opts)
+            return check_output(resize_regex, output)
+        else:
+            output = ssh(filer, cmd_opts)
+            check_output(stat_regex, output)
+            return output.split()[-1][:-1]
+
+    def options(self, filer, args):
+        """
+        TODO: Document me ...
+        """
+        pass
+
+    def rename(self, filer, args):
+        """
+        TODO: Document me ...
+        """
+        pass
+
+    def restrict(self, filer, args):
+        """
+        TODO: Document me ...
+        """
+        pass
diff --git a/func/minion/modules/netapp/vol/clone.py b/func/minion/modules/netapp/vol/clone.py
new file mode 100644 (file)
index 0000000..715d8a8
--- /dev/null
@@ -0,0 +1,46 @@
+##
+## NetApp Filer 'vol.clone' Module
+##
+## Copyright 2008, Red Hat, Inc
+## John Eckersberg <jeckersb@redhat.com>
+##
+## This software may be freely redistributed under the terms of the GNU
+## general public license.
+##
+## 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., 675 Mass Ave, Cambridge, MA 02139, USA.
+##
+
+import re
+from func.minion.modules import func_module
+from func.minion.modules.netapp.common import *
+
+class Clone(func_module.FuncModule):
+
+    # Update these if need be.
+    version = "0.0.1"
+    api_version = "0.0.1"
+    description = "Interface to the 'vol' command"
+
+    def create(self, filer, vol, parent, snap):
+        """
+        TODO: Document me ...
+        """
+        regex = """Creation of clone volume .* has completed."""
+        cmd_opts = ['vol', 'clone', 'create', vol, '-b', parent, snap]
+        output = ssh(filer, cmd_opts)
+        return check_output(regex, output)
+    
+    def split(self, filer, vol):
+        """
+        TODO: Document me ...
+        """
+        # only worry about 'start' now, I don't terribly care to automate the rest
+        regex = """Clone volume .* will be split from its parent."""
+        cmd_opts = ['vol', 'clone', 'split', 'start', vol]
+        output = ssh(filer, cmd_opts)
+        return check_output(regex, output)
+
+
+
diff --git a/func/minion/modules/networktest.py b/func/minion/modules/networktest.py
new file mode 100644 (file)
index 0000000..0e6fbb2
--- /dev/null
@@ -0,0 +1,64 @@
+# Copyright 2008, Red Hat, Inc
+# Steve 'Ashcrow' Milner <smilner@redhat.com>
+#
+# This software may be freely redistributed under the terms of the GNU
+# general public license.
+#
+# 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., 675 Mass Ave, Cambridge, MA 02139, USA.
+
+
+import func_module
+from codes import FuncException
+
+import sub_process
+
+class NetworkTest(func_module.FuncModule):
+
+    version = "0.0.1"
+    api_version = "0.0.1"
+    description = "Defines various network testing tools."
+
+    def ping(self, *args):
+        if '-c' not in args:
+            raise(FuncException("You must define a count with -c!"))
+        return self.__run_command('/bin/ping', self.__args_to_list(args))
+
+    def netstat(self, *args):
+        return self.__run_command('/bin/netstat',
+                                  self.__args_to_list(args))
+
+    def traceroute(self, *args):
+         return self.__run_command('/bin/traceroute',
+                                   self.__args_to_list(args))
+
+    def dig(self, *args):
+         return self.__run_command('/usr/bin/dig',
+                                   self.__args_to_list(args))
+
+    def isportopen(self, host, port):
+        # FIXME: the return api here needs some work... -akl
+        import socket
+        sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+        timeout = 3.0
+        sock.settimeout(timeout)
+        try:
+            sock.connect((host, int(port)))
+        except socket.error, e:
+            sock.close()
+            return [1, ("connection to %s:%s failed" % (host, port), "%s" % e)]
+        except socket.timeout:
+            sock.close()
+            return [2, ("connection to %s:%s timed out after %s seconds" % (host, port, timeout))]
+
+        sock.close()
+        return [0, "connection to %s:%s succeeded" % (host, port)]
+
+    def __args_to_list(self, args):
+        return [arg for arg in args]
+
+    def __run_command(self, command, opts=[]):
+        full_cmd = [command] + opts
+        cmd = sub_process.Popen(full_cmd, stdout=sub_process.PIPE)
+        return [line for line in cmd.communicate()[0].split('\n')]
diff --git a/func/minion/modules/process.py b/func/minion/modules/process.py
new file mode 100644 (file)
index 0000000..848e847
--- /dev/null
@@ -0,0 +1,216 @@
+## -*- coding: utf-8 -*-
+##
+## Process lister (control TBA)
+##
+## Copyright 2007, Red Hat, Inc
+## Michael DeHaan <mdehaan@redhat.com>
+##
+## This software may be freely redistributed under the terms of the GNU
+## general public license.
+##
+## 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., 675 Mass Ave, Cambridge, MA 02139, USA.
+##
+
+# other modules
+import sub_process
+import codes
+
+# our modules
+import func_module
+
+# =================================
+
+class ProcessModule(func_module.FuncModule):
+
+    version = "0.0.1"
+    api_version = "0.0.1"
+    description = "Process related reporting and control."
+
+    def info(self, flags="-auxh"):
+        """
+        Returns a struct of hardware information.  By default, this pulls down
+        all of the devices.  If you don't care about them, set with_devices to
+        False.
+        """
+
+        flags.replace(";", "") # prevent stupidity
+
+        cmd = sub_process.Popen(["/bin/ps", flags], executable="/bin/ps", 
+                                stdout=sub_process.PIPE, 
+                                stderr=sub_process.PIPE,
+                                shell=False)
+
+        data, error = cmd.communicate()
+
+        # We can get warnings for odd formatting. warnings != errors.
+        if error and error[:7] != "Warning":
+            raise codes.FuncException(error.split('\n')[0])
+
+        results = []
+        for x in data.split("\n"):
+            tokens = x.split()
+            results.append(tokens)
+
+        return results
+
+    def mem(self):
+        """
+        Returns a list of per-program memory usage.
+
+             Private  +  Shared   =  RAM used     Program
+
+           [["39.4 MiB", "10.3 MiB", "49.8 MiB",  "Xorg"],
+            ["42.2 MiB", "12.4 MiB", "54.6 MiB",  "nautilus"],
+            ["52.3 MiB", "10.8 MiB", "63.0 MiB",  "liferea-bin"]
+            ["171.6 MiB", "11.9 MiB", "183.5 MiB", "firefox-bin"]]
+
+        Taken from the ps_mem.py script written by Pádraig Brady.
+        http://www.pixelbeat.org/scripts/ps_mem.py
+        """
+        import os
+        our_pid=os.getpid()
+        results = []
+        have_smaps=0
+        have_pss=0
+
+        def kernel_ver():
+            """ (major,minor,release) """
+            kv=open("/proc/sys/kernel/osrelease").readline().split(".")[:3]
+            for char in "-_":
+                kv[2]=kv[2].split(char)[0]
+            return (int(kv[0]), int(kv[1]), int(kv[2]))
+
+        kv=kernel_ver()
+
+        def getMemStats(pid):
+            """ return Rss,Pss,Shared (note Private = Rss-Shared) """
+            Shared_lines=[]
+            Pss_lines=[]
+            pagesize=os.sysconf("SC_PAGE_SIZE")/1024 #KiB
+            Rss=int(open("/proc/"+str(pid)+"/statm").readline().split()[1])*pagesize
+            if os.path.exists("/proc/"+str(pid)+"/smaps"): #stat
+                global have_smaps
+                have_smaps=1
+                for line in open("/proc/"+str(pid)+"/smaps").readlines(): #open
+                    #Note in smaps Shared+Private = Rss above
+                    #The Rss in smaps includes video card mem etc.
+                    if line.startswith("Shared"):
+                        Shared_lines.append(line)
+                    elif line.startswith("Pss"):
+                        global have_pss
+                        have_pss=1
+                        Pss_lines.append(line)
+                Shared=sum([int(line.split()[1]) for line in Shared_lines])
+                Pss=sum([int(line.split()[1]) for line in Pss_lines])
+            elif (2,6,1) <= kv <= (2,6,9):
+                Pss=0
+                Shared=0 #lots of overestimation, but what can we do?
+            else:
+                Pss=0
+                Shared=int(open("/proc/"+str(pid)+"/statm").readline().split()[2])*pagesize
+            return (Rss, Pss, Shared)
+
+        cmds={}
+        shareds={}
+        count={}
+        for pid in os.listdir("/proc/"):
+            try:
+                pid = int(pid) #note Thread IDs not listed in /proc/
+                if pid ==our_pid: continue
+            except:
+                continue
+            cmd = file("/proc/%d/status" % pid).readline()[6:-1]
+            try:
+                exe = os.path.basename(os.path.realpath("/proc/%d/exe" % pid))
+                if exe.startswith(cmd):
+                    cmd=exe #show non truncated version
+                    #Note because we show the non truncated name
+                    #one can have separated programs as follows:
+                    #584.0 KiB + 1.0 MiB = 1.6 MiB mozilla-thunder (exe -> bash)
+                    #56.0 MiB + 22.2 MiB = 78.2 MiB mozilla-thunderbird-bin
+            except:
+                #permission denied or
+                #kernel threads don't have exe links or
+                #process gone
+                continue
+            try:
+                rss, pss, shared = getMemStats(pid)
+                private = rss-shared
+                #Note shared is always a subset of rss (trs is not always)
+            except:
+                continue #process gone
+            if shareds.get(cmd):
+                if pss: #add shared portion of PSS together
+                    shareds[cmd]+=pss-private
+                elif shareds[cmd] < shared: #just take largest shared val
+                    shareds[cmd]=shared
+            else:
+                if pss:
+                    shareds[cmd]=pss-private
+                else:
+                    shareds[cmd]=shared
+            cmds[cmd]=cmds.setdefault(cmd,0)+private
+            if count.has_key(cmd):
+               count[cmd] += 1
+            else:
+               count[cmd] = 1
+
+        #Add max shared mem for each program
+        total=0
+        for cmd in cmds.keys():
+            cmds[cmd]=cmds[cmd]+shareds[cmd]
+            total+=cmds[cmd] #valid if PSS available
+
+        sort_list = cmds.items()
+        sort_list.sort(lambda x,y:cmp(x[1],y[1]))
+        sort_list=filter(lambda x:x[1],sort_list) #get rid of zero sized processes
+
+        #The following matches "du -h" output
+        def human(num, power="Ki"):
+            powers=["Ki","Mi","Gi","Ti"]
+            while num >= 1000: #4 digits
+                num /= 1024.0
+                power=powers[powers.index(power)+1]
+            return "%.1f %s" % (num,power)
+
+        def cmd_with_count(cmd, count):
+            if count>1:
+               return "%s (%u)" % (cmd, count)
+            else:
+               return cmd
+
+        for cmd in sort_list:
+            results.append([
+                "%sB" % human(cmd[1]-shareds[cmd[0]]),
+                "%sB" % human(shareds[cmd[0]]),
+                "%sB" % human(cmd[1]),
+                "%s" % cmd_with_count(cmd[0], count[cmd[0]])
+            ])
+        if have_pss:
+            results.append(["", "", "", "%sB" % human(total)])
+
+        return results
+
+    memory = mem
+
+    def kill(self,pid,signal="TERM"):
+        if pid == "0":
+            raise codes.FuncException("Killing pid group 0 not permitted")
+        if signal == "":
+            # this is default /bin/kill behaviour, 
+            # it claims, but enfore it anyway
+            signal = "-TERM"
+        if signal[0] != "-":
+            signal = "-%s" % signal
+        rc = sub_process.call(["/bin/kill",signal, pid], 
+                              executable="/bin/kill", shell=False)
+        print rc
+        return rc
+
+    def pkill(self,name,level=""):
+        # example killall("thunderbird","-9")
+        rc = sub_process.call(["/usr/bin/pkill", name, level], 
+                              executable="/usr/bin/pkill", shell=False)
+        return rc
diff --git a/func/minion/modules/process.py.orig b/func/minion/modules/process.py.orig
new file mode 100644 (file)
index 0000000..bdd5193
--- /dev/null
@@ -0,0 +1,221 @@
+## -*- coding: utf-8 -*-
+##
+## Process lister (control TBA)
+##
+## Copyright 2007, Red Hat, Inc
+## Michael DeHaan <mdehaan@redhat.com>
+##
+## This software may be freely redistributed under the terms of the GNU
+## general public license.
+##
+## 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., 675 Mass Ave, Cambridge, MA 02139, USA.
+##
+
+# other modules
+import sub_process
+import codes
+
+# our modules
+from modules import func_module
+
+# =================================
+
+class ProcessModule(func_module.FuncModule):
+    def __init__(self):
+        self.methods = {
+            "info"    : self.info,
+            "kill"    : self.kill,
+            "pkill"   : self.pkill,
+            "mem"     : self.mem
+        }
+        func_module.FuncModule.__init__(self)
+
+    def info(self, flags="-auxh"):
+        """
+        Returns a struct of hardware information.  By default, this pulls down
+        all of the devices.  If you don't care about them, set with_devices to
+        False.
+        """
+
+        flags.replace(";", "") # prevent stupidity
+
+        cmd = sub_process.Popen(["/bin/ps", flags], executable="/bin/ps", 
+                                stdout=sub_process.PIPE, 
+                                stderr=sub_process.PIPE,
+                                shell=False)
+
+        data, error = cmd.communicate()
+
+        # We can get warnings for odd formatting. warnings != errors.
+        if error and error[:7] != "Warning":
+            raise codes.FuncException(error.split('\n')[0])
+
+        results = []
+        for x in data.split("\n"):
+            tokens = x.split()
+            results.append(tokens)
+
+        return results
+
+    def mem(self):
+        """
+        Returns a list of per-program memory usage.
+
+             Private  +  Shared   =  RAM used     Program
+
+           [["39.4 MiB", "10.3 MiB", "49.8 MiB",  "Xorg"],
+            ["42.2 MiB", "12.4 MiB", "54.6 MiB",  "nautilus"],
+            ["52.3 MiB", "10.8 MiB", "63.0 MiB",  "liferea-bin"]
+            ["171.6 MiB", "11.9 MiB", "183.5 MiB", "firefox-bin"]]
+
+        Taken from the ps_mem.py script written by Pádraig Brady.
+        http://www.pixelbeat.org/scripts/ps_mem.py
+        """
+        import os
+        our_pid=os.getpid()
+        results = []
+        have_smaps=0
+        have_pss=0
+
+        def kernel_ver():
+            """ (major,minor,release) """
+            kv=open("/proc/sys/kernel/osrelease").readline().split(".")[:3]
+            for char in "-_":
+                kv[2]=kv[2].split(char)[0]
+            return (int(kv[0]), int(kv[1]), int(kv[2]))
+
+        kv=kernel_ver()
+
+        def getMemStats(pid):
+            """ return Rss,Pss,Shared (note Private = Rss-Shared) """
+            Shared_lines=[]
+            Pss_lines=[]
+            pagesize=os.sysconf("SC_PAGE_SIZE")/1024 #KiB
+            Rss=int(open("/proc/"+str(pid)+"/statm").readline().split()[1])*pagesize
+            if os.path.exists("/proc/"+str(pid)+"/smaps"): #stat
+                global have_smaps
+                have_smaps=1
+                for line in open("/proc/"+str(pid)+"/smaps").readlines(): #open
+                    #Note in smaps Shared+Private = Rss above
+                    #The Rss in smaps includes video card mem etc.
+                    if line.startswith("Shared"):
+                        Shared_lines.append(line)
+                    elif line.startswith("Pss"):
+                        global have_pss
+                        have_pss=1
+                        Pss_lines.append(line)
+                Shared=sum([int(line.split()[1]) for line in Shared_lines])
+                Pss=sum([int(line.split()[1]) for line in Pss_lines])
+            elif (2,6,1) <= kv <= (2,6,9):
+                Pss=0
+                Shared=0 #lots of overestimation, but what can we do?
+            else:
+                Pss=0
+                Shared=int(open("/proc/"+str(pid)+"/statm").readline().split()[2])*pagesize
+            return (Rss, Pss, Shared)
+
+        cmds={}
+        shareds={}
+        count={}
+        for pid in os.listdir("/proc/"):
+            try:
+                pid = int(pid) #note Thread IDs not listed in /proc/
+                if pid ==our_pid: continue
+            except:
+                continue
+            cmd = file("/proc/%d/status" % pid).readline()[6:-1]
+            try:
+                exe = os.path.basename(os.path.realpath("/proc/%d/exe" % pid))
+                if exe.startswith(cmd):
+                    cmd=exe #show non truncated version
+                    #Note because we show the non truncated name
+                    #one can have separated programs as follows:
+                    #584.0 KiB + 1.0 MiB = 1.6 MiB mozilla-thunder (exe -> bash)
+                    #56.0 MiB + 22.2 MiB = 78.2 MiB mozilla-thunderbird-bin
+            except:
+                #permission denied or
+                #kernel threads don't have exe links or
+                #process gone
+                continue
+            try:
+                rss, pss, shared = getMemStats(pid)
+                private = rss-shared
+                #Note shared is always a subset of rss (trs is not always)
+            except:
+                continue #process gone
+            if shareds.get(cmd):
+                if pss: #add shared portion of PSS together
+                    shareds[cmd]+=pss-private
+                elif shareds[cmd] < shared: #just take largest shared val
+                    shareds[cmd]=shared
+            else:
+                if pss:
+                    shareds[cmd]=pss-private
+                else:
+                    shareds[cmd]=shared
+            cmds[cmd]=cmds.setdefault(cmd,0)+private
+            if count.has_key(cmd):
+               count[cmd] += 1
+            else:
+               count[cmd] = 1
+
+        #Add max shared mem for each program
+        total=0
+        for cmd in cmds.keys():
+            cmds[cmd]=cmds[cmd]+shareds[cmd]
+            total+=cmds[cmd] #valid if PSS available
+
+        sort_list = cmds.items()
+        sort_list.sort(lambda x,y:cmp(x[1],y[1]))
+        sort_list=filter(lambda x:x[1],sort_list) #get rid of zero sized processes
+
+        #The following matches "du -h" output
+        def human(num, power="Ki"):
+            powers=["Ki","Mi","Gi","Ti"]
+            while num >= 1000: #4 digits
+                num /= 1024.0
+                power=powers[powers.index(power)+1]
+            return "%.1f %s" % (num,power)
+
+        def cmd_with_count(cmd, count):
+            if count>1:
+               return "%s (%u)" % (cmd, count)
+            else:
+               return cmd
+
+        for cmd in sort_list:
+            results.append([
+                "%sB" % human(cmd[1]-shareds[cmd[0]]),
+                "%sB" % human(shareds[cmd[0]]),
+                "%sB" % human(cmd[1]),
+                "%s" % cmd_with_count(cmd[0], count[cmd[0]])
+            ])
+        if have_pss:
+            results.append(["", "", "", "%sB" % human(total)])
+
+        return results
+
+    def kill(self,pid,signal="TERM"):
+        if pid == "0":
+            raise codes.FuncException("Killing pid group 0 not permitted")
+        if signal == "":
+            # this is default /bin/kill behaviour, 
+            # it claims, but enfore it anyway
+            signal = "-TERM"
+        if signal[0] != "-":
+            signal = "-%s" % signal
+        rc = sub_process.call(["/bin/kill",signal, pid], 
+                              executable="/bin/kill", shell=False)
+        print rc
+        return rc
+
+    def pkill(self,name,level=""):
+        # example killall("thunderbird","-9")
+        rc = sub_process.call(["/usr/bin/pkill", name, level], 
+                              executable="/usr/bin/pkill", shell=False)
+        return rc
+
+methods = ProcessModule()
+register_rpc = methods.register_rpc
diff --git a/func/minion/modules/reboot.py b/func/minion/modules/reboot.py
new file mode 100644 (file)
index 0000000..c4fb275
--- /dev/null
@@ -0,0 +1,21 @@
+# Copyright 2007, Red Hat, Inc
+# James Bowes <jbowes@redhat.com>
+#
+# This software may be freely redistributed under the terms of the GNU
+# general public license.
+#
+# 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., 675 Mass Ave, Cambridge, MA 02139, USA.
+
+import func_module
+import sub_process
+
+class Reboot(func_module.FuncModule):
+
+    version = "0.0.1"
+    api_version = "0.0.1"
+    description = "Reboots a machine."
+
+    def reboot(self, when='now', message=''):
+        return sub_process.call(["/sbin/shutdown", '-r', when, message])
diff --git a/func/minion/modules/rpms.py b/func/minion/modules/rpms.py
new file mode 100644 (file)
index 0000000..ae26cb4
--- /dev/null
@@ -0,0 +1,44 @@
+# Copyright 2007, Red Hat, Inc
+# Michael DeHaan <mdehaan@redhat.com>
+#
+# This software may be freely redistributed under the terms of the GNU
+# general public license.
+#
+# 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., 675 Mass Ave, Cambridge, MA 02139, USA.
+
+import func_module
+import rpm
+
+class RpmModule(func_module.FuncModule):
+
+    version = "0.0.1"
+    api_version = "0.0.1"
+    description = "RPM related commands."
+
+    def inventory(self, flatten=True):
+        """
+        Returns information on all installed packages.
+        By default, 'flatten' is passed in as True, which makes printouts very
+        clean in diffs for use by func-inventory.  If you are writting another
+        software application, using flatten=False will prevent the need to 
+        parse the returns.
+        """
+        # I have not been able to get flatten=False to work if there 
+        # is more than 491 entries in the dict -- ashcrow
+        ts = rpm.TransactionSet()
+        mi = ts.dbMatch()
+        results = []
+        for hdr in mi:
+            name = hdr['name']
+            epoch = (hdr['epoch'] or 0)
+            version = hdr['version']
+            release = hdr['release']
+            arch = hdr['arch']
+            if flatten:
+                results.append("%s %s %s %s %s" % (name, epoch, version, 
+                                                   release, arch))
+            else:
+                results.append([name, epoch, version, release, arch])
+        return results
diff --git a/func/minion/modules/service.py b/func/minion/modules/service.py
new file mode 100644 (file)
index 0000000..062aea5
--- /dev/null
@@ -0,0 +1,88 @@
+## func
+##
+## Copyright 2007, Red Hat, Inc
+## Michael DeHaan <mdehaan@redhat.com>
+##
+## This software may be freely redistributed under the terms of the GNU
+## general public license.
+##
+## 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., 675 Mass Ave, Cambridge, MA 02139, USA.
+##
+##
+
+import codes
+import func_module
+
+import sub_process
+import os
+
+class Service(func_module.FuncModule):
+
+    version = "0.0.1"
+    api_version = "0.0.1"
+    description = "Allows for service control via func."
+
+    def __command(self, service_name, command):
+
+        filename = os.path.join("/etc/rc.d/init.d/",service_name)
+        if os.path.exists(filename):
+            return sub_process.call(["/sbin/service", service_name, command])
+        else:
+            raise codes.FuncException("Service not installed: %s" % service_name)
+
+    def start(self, service_name):
+        return self.__command(service_name, "start")
+
+    def stop(self, service_name):
+        return self.__command(service_name, "stop")
+
+    def restart(self, service_name):
+        return self.__command(service_name, "restart")
+
+    def reload(self, service_name):
+        return self.__command(service_name, "reload")
+
+    def status(self, service_name):
+        return self.__command(service_name, "status")
+
+    def inventory(self):
+        return {
+            "running" : self.get_running(),
+            "enabled" : self.get_enabled()
+        }
+
+    def get_enabled(self):
+        """
+        Get the list of services that are enabled at the various runlevels.  Xinetd services
+        only provide whether or not they are running, not specific runlevel info.
+        """
+
+        chkconfig = sub_process.Popen(["/sbin/chkconfig", "--list"], stdout=sub_process.PIPE)
+        data = chkconfig.communicate()[0]
+        results = []
+        for line in data.split("\n"):
+            if line.find("0:") != -1:
+               # regular services
+               tokens = line.split()
+               results.append((tokens[0],tokens[1:]))
+            elif line.find(":") != -1 and not line.endswith(":"):
+               # xinetd.d based services
+               tokens = line.split()
+               tokens[0] = tokens[0].replace(":","")
+               results.append((tokens[0],tokens[1]))
+        return results
+
+    def get_running(self):
+        """
+        Get a list of which services are running, stopped, or disabled.
+        """
+        chkconfig = sub_process.Popen(["/sbin/service", "--status-all"], stdout=sub_process.PIPE)
+        data = chkconfig.communicate()[0]
+        results = []
+        for line in data.split("\n"):
+            if line.find(" is ") != -1:
+                tokens = line.split()
+                results.append((tokens[0], tokens[-1].replace("...","")))
+        return results
diff --git a/func/minion/modules/smart.py b/func/minion/modules/smart.py
new file mode 100644 (file)
index 0000000..f410f09
--- /dev/null
@@ -0,0 +1,47 @@
+##
+## Grabs status from SMART to see if your hard drives are ok
+## Returns in the format of (return code, [line1, line2, line3,...])
+##
+## Copyright 2007, Red Hat, Inc
+## Michael DeHaan <mdehaan@redhat.com>
+##
+## This software may be freely redistributed under the terms of the GNU
+## general public license.
+##
+## 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., 675 Mass Ave, Cambridge, MA 02139, USA.
+##
+
+# other modules
+import sub_process
+
+# our modules
+import func_module
+
+# =================================
+
+class SmartModule(func_module.FuncModule):
+
+    version = "0.0.1"
+    api_version = "0.0.1"
+    description = "Grabs status from SMART to see if your hard drives are ok."
+
+    def info(self,flags="-q onecheck"):
+        """
+        Returns a struct of hardware information.  By default, this pulls down
+        all of the devices.  If you don't care about them, set with_devices to
+        False.
+        """
+
+        flags.replace(";","") # prevent stupidity
+
+        cmd = sub_process.Popen("/usr/sbin/smartd %s" % flags,stdout=sub_process.PIPE,shell=True)
+        data = cmd.communicate()[0]
+
+        results = []
+
+        for x in data.split("\n"):
+            results.append(x)
+
+        return (cmd.returncode, results)
diff --git a/func/minion/modules/snmp.py b/func/minion/modules/snmp.py
new file mode 100644 (file)
index 0000000..c992db1
--- /dev/null
@@ -0,0 +1,38 @@
+# Copyright 2007, Red Hat, Inc
+# James Bowes <jbowes@redhat.com>
+# Seth Vidal modified command.py to be snmp.py
+#
+# This software may be freely redistributed under the terms of the GNU
+# general public license.
+#
+# 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., 675 Mass Ave, Cambridge, MA 02139, USA.
+
+"""
+Abitrary command execution module for func.
+"""
+
+import func_module
+import sub_process
+base_snmp_command = '/usr/bin/snmpget -v2c -Ov -OQ'
+
+class Snmp(func_module.FuncModule):
+
+    version = "0.0.1"
+    api_version = "0.0.1"
+    description = "SNMP related calls through func."
+
+    def get(self, oid, rocommunity, hostname='localhost'):
+        """
+        Runs an snmpget on a specific oid returns the output of the call.
+        """
+        command = '%s -c %s %s %s' % (base_snmp_command, rocommunity, hostname, oid)
+        
+        cmdref = sub_process.Popen(command.split(),stdout=sub_process.PIPE,stderr=sub_process.PIPE, shell=False)
+        data = cmdref.communicate()
+        return (cmdref.returncode, data[0], data[1])
+        
+    #def walk(self, oid, rocommunity):
+
+    #def table(self, oid, rocommunity):
diff --git a/func/minion/modules/sysctl.py b/func/minion/modules/sysctl.py
new file mode 100644 (file)
index 0000000..1f11d55
--- /dev/null
@@ -0,0 +1,31 @@
+# Copyright 2008, Red Hat, Inc
+# Luke Macken <lmacken@redhat.com>
+#
+# This software may be freely redistributed under the terms of the GNU
+# general public license.
+#
+# 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., 675 Mass Ave, Cambridge, MA 02139, USA.
+
+import func_module
+import sub_process
+
+class SysctlModule(func_module.FuncModule):
+
+    version = "0.0.1"
+    description = "Configure kernel parameters at runtime"
+
+    def __run(self, cmd):
+        cmd = sub_process.Popen(cmd.split(), stdout=sub_process.PIPE,
+                                stderr=sub_process.PIPE, shell=False)
+        return [line for line in cmd.communicate()[0].strip().split('\n')]
+
+    def list(self):
+        return self.__run("/sbin/sysctl -a")
+
+    def get(self, name):
+        return self.__run("/sbin/sysctl -n %s" % name)
+
+    def set(self, name, value):
+        return self.__run("/sbin/sysctl -w %s=%s" % (name, value))
diff --git a/func/minion/modules/test.py b/func/minion/modules/test.py
new file mode 100644 (file)
index 0000000..6f7c5fa
--- /dev/null
@@ -0,0 +1,29 @@
+import func_module
+import time
+import exceptions
+
+class Test(func_module.FuncModule):
+    version = "11.11.11"
+    api_version = "0.0.1"
+    description = "Just a very simple example module"
+
+    def add(self, numb1, numb2):
+        return numb1 + numb2
+
+    def ping(self):
+        return 1
+
+    def sleep(self,t):
+        """
+        Sleeps for t seconds, and returns time of day.
+        Simply a test function for trying out async and threaded voodoo.
+        """
+        t = int(t)
+        time.sleep(t)
+        return time.time()
+
+    def explode(self):
+        """
+        Testing remote exception handling is useful
+        """
+        raise exceptions.Exception("khhhhhhaaaaaan!!!!!!")
diff --git a/func/minion/modules/virt.py b/func/minion/modules/virt.py
new file mode 100644 (file)
index 0000000..04d36bd
--- /dev/null
@@ -0,0 +1,277 @@
+"""
+Virt management features
+
+Copyright 2007, Red Hat, Inc
+Michael DeHaan <mdehaan@redhat.com>
+
+This software may be freely redistributed under the terms of the GNU
+general public license.
+
+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., 675 Mass Ave, Cambridge, MA 02139, USA.
+"""
+
+# warning: virt management is rather complicated
+# to see a simple example of func, look at the
+# service control module.  API docs on how
+# to use this to come.
+
+# other modules
+import os
+import sub_process
+import libvirt
+
+# our modules
+import codes
+import func_module
+
+VIRT_STATE_NAME_MAP = {
+   0 : "running",
+   1 : "running",
+   2 : "running",
+   3 : "paused",
+   4 : "shutdown",
+   5 : "shutdown",
+   6 : "crashed"
+}
+
+class FuncLibvirtConnection(object):
+
+    version = "0.0.1"
+    api_version = "0.0.1"
+    description = "Virtualization items through func."
+
+    def __init__(self):
+
+        cmd = sub_process.Popen("uname -r", shell=True, stdout=sub_process.PIPE)
+        output = cmd.communicate()[0]
+
+        if output.find("xen") != -1:
+            conn = libvirt.open(None)
+        else:
+            conn = libvirt.open("qemu:///system")
+
+        if not conn:
+            raise codes.FuncException("hypervisor connection failure")
+
+        self.conn = conn
+
+    def find_vm(self, vmid):
+        """
+        Extra bonus feature: vmid = -1 returns a list of everything
+        """
+        conn = self.conn
+
+        vms = []
+
+        # this block of code borrowed from virt-manager:
+        # get working domain's name
+        ids = conn.listDomainsID();
+        for id in ids:
+            vm = conn.lookupByID(id)
+            vms.append(vm)
+        # get defined domain
+        names = conn.listDefinedDomains()
+        for name in names:
+            vm = conn.lookupByName(name)
+            vms.append(vm)
+
+        if vmid == -1:
+            return vms
+
+        for vm in vms:
+            if vm.name() == vmid:
+                return vm
+
+        raise codes.FuncException("virtual machine %s not found" % vmid)
+
+    def shutdown(self, vmid):
+        return self.find_vm(vmid).shutdown()
+
+    def pause(self, vmid):
+        return self.suspend(self.conn,vmid)
+
+    def unpause(self, vmid):
+        return self.resume(self.conn,vmid)
+
+    def suspend(self, vmid):
+        return self.find_vm(vmid).suspend()
+
+    def resume(self, vmid):
+        return self.find_vm(vmid).resume()
+
+    def create(self, vmid):
+        return self.find_vm(vmid).create()
+
+    def destroy(self, vmid):
+        return self.find_vm(vmid).destroy()
+
+    def undefine(self, vmid):
+        return self.find_vm(vmid).undefine()
+
+    def get_status2(self, vm):
+        state = vm.info()[0]
+        # print "DEBUG: state: %s" % state
+        return VIRT_STATE_NAME_MAP.get(state,"unknown")
+
+    def get_status(self, vmid):
+        state = self.find_vm(vmid).info()[0]
+        return VIRT_STATE_NAME_MAP.get(state,"unknown")
+
+
+
+class Virt(func_module.FuncModule):
+
+    def __get_conn(self):
+        self.conn = FuncLibvirtConnection()
+        return self.conn
+
+    def state(self):
+        vms = self.list_vms()
+        state = []
+        for vm in vms:
+            state_blurb = self.conn.get_status(vm)
+            state.append("%s %s" % (vm,state_blurb))
+        return state
+
+
+    def info(self):
+        vms = self.list_vms()
+        info = dict()
+        for vm in vms:
+            data = self.conn.find_vm(vm).info()
+            # libvirt returns maxMem, memory, and cpuTime as long()'s, which
+            # xmlrpclib tries to convert to regular int's during serialization.
+            # This throws exceptions, so convert them to strings here and
+            # assume the other end of the xmlrpc connection can figure things
+            # out or doesn't care.
+            info[vm] = {
+                "state"     : VIRT_STATE_NAME_MAP.get(data[0],"unknown"),
+                "maxMem"    : str(data[1]),
+                "memory"    : str(data[2]),
+                "nrVirtCpu" : data[3],
+                "cpuTime"   : str(data[4])
+            }
+        return info
+
+
+    def list_vms(self):
+        self.conn = self.__get_conn()
+        vms = self.conn.find_vm(-1)
+        results = []
+        for x in vms:
+            try:
+                results.append(x.name())
+            except:
+                pass
+        return results
+
+    def install(self, server_name, target_name, system=False):
+
+        """
+        Install a new virt system by way of a named cobbler profile.
+        """
+
+        # Example:
+        # install("bootserver.example.org", "fc7webserver", True)
+
+        conn = self.__get_conn()
+
+        if conn is None:
+            raise codes.FuncException("no connection")
+
+        if not os.path.exists("/usr/bin/koan"):
+            raise codes.FuncException("no /usr/bin/koan")
+        target = "profile"
+        if system:
+            target = "system"
+
+        # TODO: FUTURE: set --virt-path in cobbler or here
+        koan_args = [
+            "/usr/bin/koan",
+            "--virt",
+            "--virt-graphics",  # enable VNC
+            "--%s=%s" % (target, target_name),
+            "--server=%s" % server_name
+        ]
+
+        rc = sub_process.call(koan_args,shell=False)
+        if rc == 0:
+            return 0
+        else:
+            raise codes.FuncException("koan returned %d" % rc)
+
+
+    def shutdown(self, vmid):
+        """
+        Make the machine with the given vmid stop running.
+        Whatever that takes.
+        """
+        self.__get_conn()
+        self.conn.shutdown(vmid)
+        return 0
+
+
+    def pause(self, vmid):
+
+        """
+        Pause the machine with the given vmid.
+        """
+        self.__get_conn()
+        self.conn.suspend(vmid)
+        return 0
+
+
+    def unpause(self, vmid):
+
+        """
+        Unpause the machine with the given vmid.
+        """
+
+        self.__get_conn()
+        self.conn.resume(vmid)
+        return 0
+
+
+    def create(self, vmid):
+
+        """
+        Start the machine via the given mac address.
+        """
+        self.__get_conn()
+        self.conn.create(vmid)
+        return 0
+
+
+    def destroy(self, vmid):
+
+        """
+        Pull the virtual power from the virtual domain, giving it virtually no
+        time to virtually shut down.
+        """
+        self.__get_conn()
+        self.conn.destroy(vmid)
+        return 0
+
+
+    def undefine(self, vmid):
+
+        """
+        Stop a domain, and then wipe it from the face of the earth.
+        by deleting the disk image and it's configuration file.
+        """
+
+        self.__get_conn()
+        self.conn.undefine(vmid)
+        return 0
+
+
+    def get_status(self, vmid):
+
+        """
+        Return a state suitable for server consumption.  Aka, codes.py values, not XM output.
+        """
+
+        self.__get_conn()
+        return self.conn.get_status(vmid)
diff --git a/func/minion/modules/yumcmd.py b/func/minion/modules/yumcmd.py
new file mode 100644 (file)
index 0000000..f952372
--- /dev/null
@@ -0,0 +1,50 @@
+# Copyright 2007, Red Hat, Inc
+# James Bowes <jbowes@redhat.com>
+#
+# This software may be freely redistributed under the terms of the GNU
+# general public license.
+#
+# 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., 675 Mass Ave, Cambridge, MA 02139, USA.
+
+import func_module
+
+import yum
+
+# XXX Use internal yum callback or write a useful one.
+class DummyCallback(object):
+
+    def event(self, state, data=None):
+        pass
+
+class Yum(func_module.FuncModule):
+
+    version = "0.0.1"
+    api_version = "0.0.1"
+    description = "Package updates through yum."
+
+    def update(self):
+        # XXX support updating specific rpms
+        ayum = yum.YumBase()
+        ayum.doGenericSetup()
+        ayum.doRepoSetup()
+        try:
+            ayum.doLock()
+            ayum.update()
+            ayum.buildTransaction()
+            ayum.processTransaction(
+                    callback=DummyCallback())
+        finally:
+            ayum.closeRpmDB()
+            ayum.doUnlock()
+        return True
+
+    def check_update(self, repo=None):
+        """Returns a list of packages due to be updated"""
+        ayum = yum.YumBase()
+        ayum.doConfigSetup()
+        ayum.doTsSetup()
+        if repo is not None:
+            ayum.repos.enableRepo(repo)
+        return map(str, ayum.doPackageLists('updates').updates)
diff --git a/func/minion/server.py b/func/minion/server.py
new file mode 100755 (executable)
index 0000000..f1b827f
--- /dev/null
@@ -0,0 +1,285 @@
+"""
+func
+
+Copyright 2007, Red Hat, Inc
+see AUTHORS
+
+This software may be freely redistributed under the terms of the GNU
+general public license.
+
+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., 675 Mass Ave, Cambridge, MA 02139, USA.
+"""
+
+# standard modules
+import SimpleXMLRPCServer
+import string
+import sys
+import traceback
+import socket
+import fnmatch
+
+from gettext import textdomain
+I18N_DOMAIN = "func"
+
+
+from func.config import read_config
+from func.commonconfig import FuncdConfig
+from func import logger
+from func import certs
+import func.jobthing as jobthing
+import utils
+
+# our modules
+import AuthedXMLRPCServer
+import codes
+import module_loader
+import func.utils as futils
+
+
+
+class XmlRpcInterface(object):
+
+    def __init__(self):
+
+        """
+        Constructor.
+        """
+
+        config_file = '/etc/func/minion.conf'
+        self.config = read_config(config_file, FuncdConfig)
+        self.logger = logger.Logger().logger
+        self.audit_logger = logger.AuditLogger()
+        self.__setup_handlers()
+        
+        # need a reference so we can log ip's, certs, etc
+        # self.server = server
+
+    def __setup_handlers(self):
+
+        """
+        Add RPC functions from each class to the global list so they can be called.
+        """
+
+        self.handlers = {}
+        for x in self.modules.keys():
+            try:
+                self.modules[x].register_rpc(self.handlers, x)
+                self.logger.debug("adding %s" % x)
+            except AttributeError, e:
+                self.logger.warning("module %s not loaded, missing register_rpc method" % self.modules[x])
+
+
+        # internal methods that we do instead of spreading internal goo
+        # all over the modules. For now, at lest -akl
+
+
+        # system.listMethods os a quasi stanard xmlrpc method, so
+        # thats why it has a odd looking name
+        self.handlers["system.listMethods"] = self.list_methods
+        self.handlers["system.list_methods"] = self.list_methods
+        self.handlers["system.list_modules"] = self.list_modules
+
+    def list_modules(self):
+        modules = self.modules.keys()
+        modules.sort()
+        return modules
+
+    def list_methods(self):
+        methods = self.handlers.keys()
+        methods.sort()
+        return methods
+
+    def get_dispatch_method(self, method):
+
+        if method in self.handlers:
+            return FuncApiMethod(self.logger, method, self.handlers[method])
+
+        else:
+            self.logger.info("Unhandled method call for method: %s " % method)
+            raise codes.InvalidMethodException
+
+
+class FuncApiMethod:
+
+    """
+    Used to hold a reference to all of the registered functions.
+    """
+
+    def __init__(self, logger, name, method):
+
+        self.logger = logger
+        self.__method = method
+        self.__name = name
+
+    def __log_exc(self):
+
+        """
+        Log an exception.
+        """
+
+        (t, v, tb) = sys.exc_info()
+        self.logger.info("Exception occured: %s" % t )
+        self.logger.info("Exception value: %s" % v)
+        self.logger.info("Exception Info:\n%s" % string.join(traceback.format_list(traceback.extract_tb(tb))))
+
+    def __call__(self, *args):
+
+        self.logger.debug("(X) -------------------------------------------")
+
+        try:
+            rc = self.__method(*args)
+        except codes.FuncException, e:
+            self.__log_exc()
+            (t, v, tb) = sys.exc_info()
+            rc = futils.nice_exception(t,v,tb)
+        except:
+            self.__log_exc()
+            (t, v, tb) = sys.exc_info()
+            rc = futils.nice_exception(t,v,tb)
+        self.logger.debug("Return code for %s: %s" % (self.__name, rc))
+
+        return rc
+
+
+def serve():
+
+    """
+    Code for starting the XMLRPC service.
+    """
+    server =FuncSSLXMLRPCServer(('', 51234))
+    server.logRequests = 0 # don't print stuff to console
+    server.serve_forever()
+
+
+
+class FuncXMLRPCServer(SimpleXMLRPCServer.SimpleXMLRPCServer, XmlRpcInterface):
+
+    def __init__(self, args):
+
+        self.allow_reuse_address = True
+
+        self.modules = module_loader.load_modules()
+        SimpleXMLRPCServer.SimpleXMLRPCServer.__init__(self, args)
+        XmlRpcInterface.__init__(self)
+
+
+class FuncSSLXMLRPCServer(AuthedXMLRPCServer.AuthedSSLXMLRPCServer,
+                          XmlRpcInterface):
+    def __init__(self, args):
+        self.allow_reuse_address = True
+        self.modules = module_loader.load_modules()
+
+        XmlRpcInterface.__init__(self)
+        hn = utils.get_hostname()
+        self.key = "%s/%s.pem" % (self.config.cert_dir, hn)
+        self.cert = "%s/%s.cert" % (self.config.cert_dir, hn)
+        self.ca = "%s/ca.cert" % self.config.cert_dir
+        
+        self._our_ca = certs.retrieve_cert_from_file(self.ca)
+        
+        AuthedXMLRPCServer.AuthedSSLXMLRPCServer.__init__(self, ("", 51234),
+                                                          self.key, self.cert,
+                                                          self.ca)
+
+    def _dispatch(self, method, params):
+
+        """
+        the SimpleXMLRPCServer class will call _dispatch if it doesn't
+        find a handler method
+        """
+        # take _this_request and hand it off to check out the acls of the method
+        # being called vs the requesting host
+        
+        if not hasattr(self, '_this_request'):
+            raise codes.InvalidMethodException
+            
+        r,a = self._this_request
+        peer_cert = r.get_peer_certificate()
+        ip = a[0]
+        
+
+        # generally calling conventions are:  hardware.info
+        # async convention is async.hardware.info
+        # here we parse out the async to decide how to invoke it.
+        # see the async docs on the Wiki for further info.
+        async_dispatch = False
+        if method.startswith("async."):
+            async_dispatch = True
+            method = method.replace("async.","",1)
+
+        if not self._check_acl(peer_cert, ip, method, params):
+            raise codes.AccessToMethodDenied
+            
+        # Recognize ipython's tab completion calls
+        if method == 'trait_names' or method == '_getAttributeNames':
+            return self.handlers.keys()
+
+        cn = peer_cert.get_subject().CN
+        sub_hash = peer_cert.subject_name_hash()
+        self.audit_logger.log_call(ip, cn, sub_hash, method, params)
+
+        try:
+            if not async_dispatch:
+                return self.get_dispatch_method(method)(*params)
+            else:
+                return jobthing.minion_async_run(self.get_dispatch_method, method, params)
+        except:
+            (t, v, tb) = sys.exc_info()
+            rc = futils.nice_exception(t, v, tb)
+            return rc
+
+    def auth_cb(self, request, client_address):
+        peer_cert = request.get_peer_certificate()
+        return peer_cert.get_subject().CN
+    
+    def _check_acl(self, cert, ip, method, params):
+        acls = utils.get_acls_from_config(acldir=self.config.acl_dir)
+        
+        # certmaster always gets to run things
+        ca_cn = self._our_ca.get_subject().CN
+        ca_hash = self._our_ca.subject_name_hash()
+        ca_key = '%s-%s' % (ca_cn, ca_hash)
+        acls[ca_key] = ['*']
+
+        cn = cert.get_subject().CN
+        sub_hash = cert.subject_name_hash()
+        if acls:
+            allow_list = []
+            hostkey = '%s-%s' % (cn, sub_hash)
+            # search all the keys, match to 'cn-subhash'
+            for hostmatch in acls.keys():
+                if fnmatch.fnmatch(hostkey, hostmatch):
+                    allow_list.extend(acls[hostmatch])
+            # go through the allow_list and make sure this method is in there
+            for methodmatch in allow_list:
+                if fnmatch.fnmatch(method, methodmatch):
+                    return True
+                    
+        return False
+
+
+def main(argv):
+
+    """
+    Start things up.
+    """
+
+    if "daemon" in sys.argv or "--daemon" in sys.argv:
+        futils.daemonize("/var/run/funcd.pid")
+    else:
+        print "serving...\n"
+
+    try:
+        utils.create_minion_keys()
+        serve()
+    except codes.FuncException, e:
+        print >> sys.stderr, 'error: %s' % e
+        sys.exit(1)
+
+
+# ======================================================================================
+if __name__ == "__main__":
+    textdomain(I18N_DOMAIN)
+    main(sys.argv)
diff --git a/func/minion/sub_process.py b/func/minion/sub_process.py
new file mode 100644 (file)
index 0000000..351a951
--- /dev/null
@@ -0,0 +1,1221 @@
+# subprocess - Subprocesses with accessible I/O streams
+#
+# For more information about this module, see PEP 324.
+#
+# This module should remain compatible with Python 2.2, see PEP 291.
+#
+# Copyright (c) 2003-2005 by Peter Astrand <astrand@lysator.liu.se>
+#
+# Licensed to PSF under a Contributor Agreement.
+# See http://www.python.org/2.4/license for licensing details.
+
+r"""subprocess - Subprocesses with accessible I/O streams
+
+This module allows you to spawn processes, connect to their
+input/output/error pipes, and obtain their return codes.  This module
+intends to replace several other, older modules and functions, like:
+
+os.system
+os.spawn*
+os.popen*
+popen2.*
+commands.*
+
+Information about how the subprocess module can be used to replace these
+modules and functions can be found below.
+
+
+
+Using the subprocess module
+===========================
+This module defines one class called Popen:
+
+class Popen(args, bufsize=0, executable=None,
+            stdin=None, stdout=None, stderr=None,
+            preexec_fn=None, close_fds=False, shell=False,
+            cwd=None, env=None, universal_newlines=False,
+            startupinfo=None, creationflags=0):
+
+
+Arguments are:
+
+args should be a string, or a sequence of program arguments.  The
+program to execute is normally the first item in the args sequence or
+string, but can be explicitly set by using the executable argument.
+
+On UNIX, with shell=False (default): In this case, the Popen class
+uses os.execvp() to execute the child program.  args should normally
+be a sequence.  A string will be treated as a sequence with the string
+as the only item (the program to execute).
+
+On UNIX, with shell=True: If args is a string, it specifies the
+command string to execute through the shell.  If args is a sequence,
+the first item specifies the command string, and any additional items
+will be treated as additional shell arguments.
+
+On Windows: the Popen class uses CreateProcess() to execute the child
+program, which operates on strings.  If args is a sequence, it will be
+converted to a string using the list2cmdline method.  Please note that
+not all MS Windows applications interpret the command line the same
+way: The list2cmdline is designed for applications using the same
+rules as the MS C runtime.
+
+bufsize, if given, has the same meaning as the corresponding argument
+to the built-in open() function: 0 means unbuffered, 1 means line
+buffered, any other positive value means use a buffer of
+(approximately) that size.  A negative bufsize means to use the system
+default, which usually means fully buffered.  The default value for
+bufsize is 0 (unbuffered).
+
+stdin, stdout and stderr specify the executed programs' standard
+input, standard output and standard error file handles, respectively.
+Valid values are PIPE, an existing file descriptor (a positive
+integer), an existing file object, and None.  PIPE indicates that a
+new pipe to the child should be created.  With None, no redirection
+will occur; the child's file handles will be inherited from the
+parent.  Additionally, stderr can be STDOUT, which indicates that the
+stderr data from the applications should be captured into the same
+file handle as for stdout.
+
+If preexec_fn is set to a callable object, this object will be called
+in the child process just before the child is executed.
+
+If close_fds is true, all file descriptors except 0, 1 and 2 will be
+closed before the child process is executed.
+
+if shell is true, the specified command will be executed through the
+shell.
+
+If cwd is not None, the current directory will be changed to cwd
+before the child is executed.
+
+If env is not None, it defines the environment variables for the new
+process.
+
+If universal_newlines is true, the file objects stdout and stderr are
+opened as a text files, but lines may be terminated by any of '\n',
+the Unix end-of-line convention, '\r', the Macintosh convention or
+'\r\n', the Windows convention.  All of these external representations
+are seen as '\n' by the Python program.  Note: This feature is only
+available if Python is built with universal newline support (the
+default).  Also, the newlines attribute of the file objects stdout,
+stdin and stderr are not updated by the communicate() method.
+
+The startupinfo and creationflags, if given, will be passed to the
+underlying CreateProcess() function.  They can specify things such as
+appearance of the main window and priority for the new process.
+(Windows only)
+
+
+This module also defines two shortcut functions:
+
+call(*popenargs, **kwargs):
+    Run command with arguments.  Wait for command to complete, then
+    return the returncode attribute.
+
+    The arguments are the same as for the Popen constructor.  Example:
+
+    retcode = call(["ls", "-l"])
+
+check_call(*popenargs, **kwargs):
+    Run command with arguments.  Wait for command to complete.  If the
+    exit code was zero then return, otherwise raise
+    CalledProcessError.  The CalledProcessError object will have the
+    return code in the returncode attribute.
+
+    The arguments are the same as for the Popen constructor.  Example:
+
+    check_call(["ls", "-l"])
+
+Exceptions
+----------
+Exceptions raised in the child process, before the new program has
+started to execute, will be re-raised in the parent.  Additionally,
+the exception object will have one extra attribute called
+'child_traceback', which is a string containing traceback information
+from the childs point of view.
+
+The most common exception raised is OSError.  This occurs, for
+example, when trying to execute a non-existent file.  Applications
+should prepare for OSErrors.
+
+A ValueError will be raised if Popen is called with invalid arguments.
+
+check_call() will raise CalledProcessError, if the called process
+returns a non-zero return code.
+
+
+Security
+--------
+Unlike some other popen functions, this implementation will never call
+/bin/sh implicitly.  This means that all characters, including shell
+metacharacters, can safely be passed to child processes.
+
+
+Popen objects
+=============
+Instances of the Popen class have the following methods:
+
+poll()
+    Check if child process has terminated.  Returns returncode
+    attribute.
+
+wait()
+    Wait for child process to terminate.  Returns returncode attribute.
+
+communicate(input=None)
+    Interact with process: Send data to stdin.  Read data from stdout
+    and stderr, until end-of-file is reached.  Wait for process to
+    terminate.  The optional stdin argument should be a string to be
+    sent to the child process, or None, if no data should be sent to
+    the child.
+
+    communicate() returns a tuple (stdout, stderr).
+
+    Note: The data read is buffered in memory, so do not use this
+    method if the data size is large or unlimited.
+
+The following attributes are also available:
+
+stdin
+    If the stdin argument is PIPE, this attribute is a file object
+    that provides input to the child process.  Otherwise, it is None.
+
+stdout
+    If the stdout argument is PIPE, this attribute is a file object
+    that provides output from the child process.  Otherwise, it is
+    None.
+
+stderr
+    If the stderr argument is PIPE, this attribute is file object that
+    provides error output from the child process.  Otherwise, it is
+    None.
+
+pid
+    The process ID of the child process.
+
+returncode
+    The child return code.  A None value indicates that the process
+    hasn't terminated yet.  A negative value -N indicates that the
+    child was terminated by signal N (UNIX only).
+
+
+Replacing older functions with the subprocess module
+====================================================
+In this section, "a ==> b" means that b can be used as a replacement
+for a.
+
+Note: All functions in this section fail (more or less) silently if
+the executed program cannot be found; this module raises an OSError
+exception.
+
+In the following examples, we assume that the subprocess module is
+imported with "from subprocess import *".
+
+
+Replacing /bin/sh shell backquote
+---------------------------------
+output=`mycmd myarg`
+==>
+output = Popen(["mycmd", "myarg"], stdout=PIPE).communicate()[0]
+
+
+Replacing shell pipe line
+-------------------------
+output=`dmesg | grep hda`
+==>
+p1 = Popen(["dmesg"], stdout=PIPE)
+p2 = Popen(["grep", "hda"], stdin=p1.stdout, stdout=PIPE)
+output = p2.communicate()[0]
+
+
+Replacing os.system()
+---------------------
+sts = os.system("mycmd" + " myarg")
+==>
+p = Popen("mycmd" + " myarg", shell=True)
+pid, sts = os.waitpid(p.pid, 0)
+
+Note:
+
+* Calling the program through the shell is usually not required.
+
+* It's easier to look at the returncode attribute than the
+  exitstatus.
+
+A more real-world example would look like this:
+
+try:
+    retcode = call("mycmd" + " myarg", shell=True)
+    if retcode < 0:
+        print >>sys.stderr, "Child was terminated by signal", -retcode
+    else:
+        print >>sys.stderr, "Child returned", retcode
+except OSError, e:
+    print >>sys.stderr, "Execution failed:", e
+
+
+Replacing os.spawn*
+-------------------
+P_NOWAIT example:
+
+pid = os.spawnlp(os.P_NOWAIT, "/bin/mycmd", "mycmd", "myarg")
+==>
+pid = Popen(["/bin/mycmd", "myarg"]).pid
+
+
+P_WAIT example:
+
+retcode = os.spawnlp(os.P_WAIT, "/bin/mycmd", "mycmd", "myarg")
+==>
+retcode = call(["/bin/mycmd", "myarg"])
+
+
+Vector example:
+
+os.spawnvp(os.P_NOWAIT, path, args)
+==>
+Popen([path] + args[1:])
+
+
+Environment example:
+
+os.spawnlpe(os.P_NOWAIT, "/bin/mycmd", "mycmd", "myarg", env)
+==>
+Popen(["/bin/mycmd", "myarg"], env={"PATH": "/usr/bin"})
+
+
+Replacing os.popen*
+-------------------
+pipe = os.popen(cmd, mode='r', bufsize)
+==>
+pipe = Popen(cmd, shell=True, bufsize=bufsize, stdout=PIPE).stdout
+
+pipe = os.popen(cmd, mode='w', bufsize)
+==>
+pipe = Popen(cmd, shell=True, bufsize=bufsize, stdin=PIPE).stdin
+
+
+(child_stdin, child_stdout) = os.popen2(cmd, mode, bufsize)
+==>
+p = Popen(cmd, shell=True, bufsize=bufsize,
+          stdin=PIPE, stdout=PIPE, close_fds=True)
+(child_stdin, child_stdout) = (p.stdin, p.stdout)
+
+
+(child_stdin,
+ child_stdout,
+ child_stderr) = os.popen3(cmd, mode, bufsize)
+==>
+p = Popen(cmd, shell=True, bufsize=bufsize,
+          stdin=PIPE, stdout=PIPE, stderr=PIPE, close_fds=True)
+(child_stdin,
+ child_stdout,
+ child_stderr) = (p.stdin, p.stdout, p.stderr)
+
+
+(child_stdin, child_stdout_and_stderr) = os.popen4(cmd, mode, bufsize)
+==>
+p = Popen(cmd, shell=True, bufsize=bufsize,
+          stdin=PIPE, stdout=PIPE, stderr=STDOUT, close_fds=True)
+(child_stdin, child_stdout_and_stderr) = (p.stdin, p.stdout)
+
+
+Replacing popen2.*
+------------------
+Note: If the cmd argument to popen2 functions is a string, the command
+is executed through /bin/sh.  If it is a list, the command is directly
+executed.
+
+(child_stdout, child_stdin) = popen2.popen2("somestring", bufsize, mode)
+==>
+p = Popen(["somestring"], shell=True, bufsize=bufsize
+          stdin=PIPE, stdout=PIPE, close_fds=True)
+(child_stdout, child_stdin) = (p.stdout, p.stdin)
+
+
+(child_stdout, child_stdin) = popen2.popen2(["mycmd", "myarg"], bufsize, mode)
+==>
+p = Popen(["mycmd", "myarg"], bufsize=bufsize,
+          stdin=PIPE, stdout=PIPE, close_fds=True)
+(child_stdout, child_stdin) = (p.stdout, p.stdin)
+
+The popen2.Popen3 and popen3.Popen4 basically works as subprocess.Popen,
+except that:
+
+* subprocess.Popen raises an exception if the execution fails
+* the capturestderr argument is replaced with the stderr argument.
+* stdin=PIPE and stdout=PIPE must be specified.
+* popen2 closes all filedescriptors by default, but you have to specify
+  close_fds=True with subprocess.Popen.
+
+
+"""
+
+import sys
+mswindows = (sys.platform == "win32")
+
+import os
+import types
+import traceback
+
+# Exception classes used by this module.
+class CalledProcessError(Exception):
+    """This exception is raised when a process run by check_call() returns
+    a non-zero exit status.  The exit status will be stored in the
+    returncode attribute."""
+    def __init__(self, returncode, cmd):
+        self.returncode = returncode
+        self.cmd = cmd
+    def __str__(self):
+        return "Command '%s' returned non-zero exit status %d" % (self.cmd, self.returncode)
+
+
+if mswindows:
+    import threading
+    import msvcrt
+    if 0: # <-- change this to use pywin32 instead of the _subprocess driver
+        import pywintypes
+        from win32api import GetStdHandle, STD_INPUT_HANDLE, \
+                             STD_OUTPUT_HANDLE, STD_ERROR_HANDLE
+        from win32api import GetCurrentProcess, DuplicateHandle, \
+                             GetModuleFileName, GetVersion
+        from win32con import DUPLICATE_SAME_ACCESS, SW_HIDE
+        from win32pipe import CreatePipe
+        from win32process import CreateProcess, STARTUPINFO, \
+                                 GetExitCodeProcess, STARTF_USESTDHANDLES, \
+                                 STARTF_USESHOWWINDOW, CREATE_NEW_CONSOLE
+        from win32event import WaitForSingleObject, INFINITE, WAIT_OBJECT_0
+    else:
+        from _subprocess import *
+        class STARTUPINFO:
+            dwFlags = 0
+            hStdInput = None
+            hStdOutput = None
+            hStdError = None
+            wShowWindow = 0
+        class pywintypes:
+            error = IOError
+else:
+    import select
+    import errno
+    import fcntl
+    import pickle
+
+__all__ = ["Popen", "PIPE", "STDOUT", "call", "check_call", "CalledProcessError"]
+
+try:
+    MAXFD = os.sysconf("SC_OPEN_MAX")
+except:
+    MAXFD = 256
+
+# True/False does not exist on 2.2.0
+try:
+    False
+except NameError:
+    False = 0
+    True = 1
+
+_active = []
+
+def _cleanup():
+    for inst in _active[:]:
+        if inst.poll(_deadstate=sys.maxint) >= 0:
+            try:
+                _active.remove(inst)
+            except ValueError:
+                # This can happen if two threads create a new Popen instance.
+                # It's harmless that it was already removed, so ignore.
+                pass
+
+PIPE = -1
+STDOUT = -2
+
+
+def call(*popenargs, **kwargs):
+    """Run command with arguments.  Wait for command to complete, then
+    return the returncode attribute.
+
+    The arguments are the same as for the Popen constructor.  Example:
+
+    retcode = call(["ls", "-l"])
+    """
+    return Popen(*popenargs, **kwargs).wait()
+
+
+def check_call(*popenargs, **kwargs):
+    """Run command with arguments.  Wait for command to complete.  If
+    the exit code was zero then return, otherwise raise
+    CalledProcessError.  The CalledProcessError object will have the
+    return code in the returncode attribute.
+
+    The arguments are the same as for the Popen constructor.  Example:
+
+    check_call(["ls", "-l"])
+    """
+    retcode = call(*popenargs, **kwargs)
+    cmd = kwargs.get("args")
+    if cmd is None:
+        cmd = popenargs[0]
+    if retcode:
+        raise CalledProcessError(retcode, cmd)
+    return retcode
+
+
+def list2cmdline(seq):
+    """
+    Translate a sequence of arguments into a command line
+    string, using the same rules as the MS C runtime:
+
+    1) Arguments are delimited by white space, which is either a
+       space or a tab.
+
+    2) A string surrounded by double quotation marks is
+       interpreted as a single argument, regardless of white space
+       contained within.  A quoted string can be embedded in an
+       argument.
+
+    3) A double quotation mark preceded by a backslash is
+       interpreted as a literal double quotation mark.
+
+    4) Backslashes are interpreted literally, unless they
+       immediately precede a double quotation mark.
+
+    5) If backslashes immediately precede a double quotation mark,
+       every pair of backslashes is interpreted as a literal
+       backslash.  If the number of backslashes is odd, the last
+       backslash escapes the next double quotation mark as
+       described in rule 3.
+    """
+
+    # See
+    # http://msdn.microsoft.com/library/en-us/vccelng/htm/progs_12.asp
+    result = []
+    needquote = False
+    for arg in seq:
+        bs_buf = []
+
+        # Add a space to separate this argument from the others
+        if result:
+            result.append(' ')
+
+        needquote = (" " in arg) or ("\t" in arg)
+        if needquote:
+            result.append('"')
+
+        for c in arg:
+            if c == '\\':
+                # Don't know if we need to double yet.
+                bs_buf.append(c)
+            elif c == '"':
+                # Double backspaces.
+                result.append('\\' * len(bs_buf)*2)
+                bs_buf = []
+                result.append('\\"')
+            else:
+                # Normal char
+                if bs_buf:
+                    result.extend(bs_buf)
+                    bs_buf = []
+                result.append(c)
+
+        # Add remaining backspaces, if any.
+        if bs_buf:
+            result.extend(bs_buf)
+
+        if needquote:
+            result.extend(bs_buf)
+            result.append('"')
+
+    return ''.join(result)
+
+
+class Popen(object):
+    def __init__(self, args, bufsize=0, executable=None,
+                 stdin=None, stdout=None, stderr=None,
+                 preexec_fn=None, close_fds=False, shell=False,
+                 cwd=None, env=None, universal_newlines=False,
+                 startupinfo=None, creationflags=0):
+        """Create new Popen instance."""
+        _cleanup()
+
+        self._child_created = False
+        if not isinstance(bufsize, (int, long)):
+            raise TypeError("bufsize must be an integer")
+
+        if mswindows:
+            if preexec_fn is not None:
+                raise ValueError("preexec_fn is not supported on Windows "
+                                 "platforms")
+            if close_fds:
+                raise ValueError("close_fds is not supported on Windows "
+                                 "platforms")
+        else:
+            # POSIX
+            if startupinfo is not None:
+                raise ValueError("startupinfo is only supported on Windows "
+                                 "platforms")
+            if creationflags != 0:
+                raise ValueError("creationflags is only supported on Windows "
+                                 "platforms")
+
+        self.stdin = None
+        self.stdout = None
+        self.stderr = None
+        self.pid = None
+        self.returncode = None
+        self.universal_newlines = universal_newlines
+
+        # Input and output objects. The general principle is like
+        # this:
+        #
+        # Parent                   Child
+        # ------                   -----
+        # p2cwrite   ---stdin--->  p2cread
+        # c2pread    <--stdout---  c2pwrite
+        # errread    <--stderr---  errwrite
+        #
+        # On POSIX, the child objects are file descriptors.  On
+        # Windows, these are Windows file handles.  The parent objects
+        # are file descriptors on both platforms.  The parent objects
+        # are None when not using PIPEs. The child objects are None
+        # when not redirecting.
+
+        (p2cread, p2cwrite,
+         c2pread, c2pwrite,
+         errread, errwrite) = self._get_handles(stdin, stdout, stderr)
+
+        self._execute_child(args, executable, preexec_fn, close_fds,
+                            cwd, env, universal_newlines,
+                            startupinfo, creationflags, shell,
+                            p2cread, p2cwrite,
+                            c2pread, c2pwrite,
+                            errread, errwrite)
+
+        if p2cwrite:
+            self.stdin = os.fdopen(p2cwrite, 'wb', bufsize)
+        if c2pread:
+            if universal_newlines:
+                self.stdout = os.fdopen(c2pread, 'rU', bufsize)
+            else:
+                self.stdout = os.fdopen(c2pread, 'rb', bufsize)
+        if errread:
+            if universal_newlines:
+                self.stderr = os.fdopen(errread, 'rU', bufsize)
+            else:
+                self.stderr = os.fdopen(errread, 'rb', bufsize)
+
+
+    def _translate_newlines(self, data):
+        data = data.replace("\r\n", "\n")
+        data = data.replace("\r", "\n")
+        return data
+
+
+    def __del__(self):
+        if not self._child_created:
+            # We didn't get to successfully create a child process.
+            return
+        # In case the child hasn't been waited on, check if it's done.
+        self.poll(_deadstate=sys.maxint)
+        if self.returncode is None and _active is not None:
+            # Child is still running, keep us alive until we can wait on it.
+            _active.append(self)
+
+
+    def communicate(self, input=None):
+        """Interact with process: Send data to stdin.  Read data from
+        stdout and stderr, until end-of-file is reached.  Wait for
+        process to terminate.  The optional input argument should be a
+        string to be sent to the child process, or None, if no data
+        should be sent to the child.
+
+        communicate() returns a tuple (stdout, stderr)."""
+
+        # Optimization: If we are only using one pipe, or no pipe at
+        # all, using select() or threads is unnecessary.
+        if [self.stdin, self.stdout, self.stderr].count(None) >= 2:
+            stdout = None
+            stderr = None
+            if self.stdin:
+                if input:
+                    self.stdin.write(input)
+                self.stdin.close()
+            elif self.stdout:
+                stdout = self.stdout.read()
+            elif self.stderr:
+                stderr = self.stderr.read()
+            self.wait()
+            return (stdout, stderr)
+
+        return self._communicate(input)
+
+
+    if mswindows:
+        #
+        # Windows methods
+        #
+        def _get_handles(self, stdin, stdout, stderr):
+            """Construct and return tupel with IO objects:
+            p2cread, p2cwrite, c2pread, c2pwrite, errread, errwrite
+            """
+            if stdin is None and stdout is None and stderr is None:
+                return (None, None, None, None, None, None)
+
+            p2cread, p2cwrite = None, None
+            c2pread, c2pwrite = None, None
+            errread, errwrite = None, None
+
+            if stdin is None:
+                p2cread = GetStdHandle(STD_INPUT_HANDLE)
+            elif stdin == PIPE:
+                p2cread, p2cwrite = CreatePipe(None, 0)
+                # Detach and turn into fd
+                p2cwrite = p2cwrite.Detach()
+                p2cwrite = msvcrt.open_osfhandle(p2cwrite, 0)
+            elif isinstance(stdin, int):
+                p2cread = msvcrt.get_osfhandle(stdin)
+            else:
+                # Assuming file-like object
+                p2cread = msvcrt.get_osfhandle(stdin.fileno())
+            p2cread = self._make_inheritable(p2cread)
+
+            if stdout is None:
+                c2pwrite = GetStdHandle(STD_OUTPUT_HANDLE)
+            elif stdout == PIPE:
+                c2pread, c2pwrite = CreatePipe(None, 0)
+                # Detach and turn into fd
+                c2pread = c2pread.Detach()
+                c2pread = msvcrt.open_osfhandle(c2pread, 0)
+            elif isinstance(stdout, int):
+                c2pwrite = msvcrt.get_osfhandle(stdout)
+            else:
+                # Assuming file-like object
+                c2pwrite = msvcrt.get_osfhandle(stdout.fileno())
+            c2pwrite = self._make_inheritable(c2pwrite)
+
+            if stderr is None:
+                errwrite = GetStdHandle(STD_ERROR_HANDLE)
+            elif stderr == PIPE:
+                errread, errwrite = CreatePipe(None, 0)
+                # Detach and turn into fd
+                errread = errread.Detach()
+                errread = msvcrt.open_osfhandle(errread, 0)
+            elif stderr == STDOUT:
+                errwrite = c2pwrite
+            elif isinstance(stderr, int):
+                errwrite = msvcrt.get_osfhandle(stderr)
+            else:
+                # Assuming file-like object
+                errwrite = msvcrt.get_osfhandle(stderr.fileno())
+            errwrite = self._make_inheritable(errwrite)
+
+            return (p2cread, p2cwrite,
+                    c2pread, c2pwrite,
+                    errread, errwrite)
+
+
+        def _make_inheritable(self, handle):
+            """Return a duplicate of handle, which is inheritable"""
+            return DuplicateHandle(GetCurrentProcess(), handle,
+                                   GetCurrentProcess(), 0, 1,
+                                   DUPLICATE_SAME_ACCESS)
+
+
+        def _find_w9xpopen(self):
+            """Find and return absolut path to w9xpopen.exe"""
+            w9xpopen = os.path.join(os.path.dirname(GetModuleFileName(0)),
+                                    "w9xpopen.exe")
+            if not os.path.exists(w9xpopen):
+                # Eeek - file-not-found - possibly an embedding
+                # situation - see if we can locate it in sys.exec_prefix
+                w9xpopen = os.path.join(os.path.dirname(sys.exec_prefix),
+                                        "w9xpopen.exe")
+                if not os.path.exists(w9xpopen):
+                    raise RuntimeError("Cannot locate w9xpopen.exe, which is "
+                                       "needed for Popen to work with your "
+                                       "shell or platform.")
+            return w9xpopen
+
+
+        def _execute_child(self, args, executable, preexec_fn, close_fds,
+                           cwd, env, universal_newlines,
+                           startupinfo, creationflags, shell,
+                           p2cread, p2cwrite,
+                           c2pread, c2pwrite,
+                           errread, errwrite):
+            """Execute program (MS Windows version)"""
+
+            if not isinstance(args, types.StringTypes):
+                args = list2cmdline(args)
+
+            # Process startup details
+            if startupinfo is None:
+                startupinfo = STARTUPINFO()
+            if None not in (p2cread, c2pwrite, errwrite):
+                startupinfo.dwFlags |= STARTF_USESTDHANDLES
+                startupinfo.hStdInput = p2cread
+                startupinfo.hStdOutput = c2pwrite
+                startupinfo.hStdError = errwrite
+
+            if shell:
+                startupinfo.dwFlags |= STARTF_USESHOWWINDOW
+                startupinfo.wShowWindow = SW_HIDE
+                comspec = os.environ.get("COMSPEC", "cmd.exe")
+                args = comspec + " /c " + args
+                if (GetVersion() >= 0x80000000L or
+                        os.path.basename(comspec).lower() == "command.com"):
+                    # Win9x, or using command.com on NT. We need to
+                    # use the w9xpopen intermediate program. For more
+                    # information, see KB Q150956
+                    # (http://web.archive.org/web/20011105084002/http://support.microsoft.com/support/kb/articles/Q150/9/56.asp)
+                    w9xpopen = self._find_w9xpopen()
+                    args = '"%s" %s' % (w9xpopen, args)
+                    # Not passing CREATE_NEW_CONSOLE has been known to
+                    # cause random failures on win9x.  Specifically a
+                    # dialog: "Your program accessed mem currently in
+                    # use at xxx" and a hopeful warning about the
+                    # stability of your system.  Cost is Ctrl+C wont
+                    # kill children.
+                    creationflags |= CREATE_NEW_CONSOLE
+
+            # Start the process
+            try:
+                hp, ht, pid, tid = CreateProcess(executable, args,
+                                         # no special security
+                                         None, None,
+                                         # must inherit handles to pass std
+                                         # handles
+                                         1,
+                                         creationflags,
+                                         env,
+                                         cwd,
+                                         startupinfo)
+            except pywintypes.error, e:
+                # Translate pywintypes.error to WindowsError, which is
+                # a subclass of OSError.  FIXME: We should really
+                # translate errno using _sys_errlist (or simliar), but
+                # how can this be done from Python?
+                raise WindowsError(*e.args)
+
+            # Retain the process handle, but close the thread handle
+            self._child_created = True
+            self._handle = hp
+            self.pid = pid
+            ht.Close()
+
+            # Child is launched. Close the parent's copy of those pipe
+            # handles that only the child should have open.  You need
+            # to make sure that no handles to the write end of the
+            # output pipe are maintained in this process or else the
+            # pipe will not close when the child process exits and the
+            # ReadFile will hang.
+            if p2cread is not None:
+                p2cread.Close()
+            if c2pwrite is not None:
+                c2pwrite.Close()
+            if errwrite is not None:
+                errwrite.Close()
+
+
+        def poll(self, _deadstate=None):
+            """Check if child process has terminated.  Returns returncode
+            attribute."""
+            if self.returncode is None:
+                if WaitForSingleObject(self._handle, 0) == WAIT_OBJECT_0:
+                    self.returncode = GetExitCodeProcess(self._handle)
+            return self.returncode
+
+
+        def wait(self):
+            """Wait for child process to terminate.  Returns returncode
+            attribute."""
+            if self.returncode is None:
+                obj = WaitForSingleObject(self._handle, INFINITE)
+                self.returncode = GetExitCodeProcess(self._handle)
+            return self.returncode
+
+
+        def _readerthread(self, fh, buffer):
+            buffer.append(fh.read())
+
+
+        def _communicate(self, input):
+            stdout = None # Return
+            stderr = None # Return
+
+            if self.stdout:
+                stdout = []
+                stdout_thread = threading.Thread(target=self._readerthread,
+                                                 args=(self.stdout, stdout))
+                stdout_thread.setDaemon(True)
+                stdout_thread.start()
+            if self.stderr:
+                stderr = []
+                stderr_thread = threading.Thread(target=self._readerthread,
+                                                 args=(self.stderr, stderr))
+                stderr_thread.setDaemon(True)
+                stderr_thread.start()
+
+            if self.stdin:
+                if input is not None:
+                    self.stdin.write(input)
+                self.stdin.close()
+
+            if self.stdout:
+                stdout_thread.join()
+            if self.stderr:
+                stderr_thread.join()
+
+            # All data exchanged.  Translate lists into strings.
+            if stdout is not None:
+                stdout = stdout[0]
+            if stderr is not None:
+                stderr = stderr[0]
+
+            # Translate newlines, if requested.  We cannot let the file
+            # object do the translation: It is based on stdio, which is
+            # impossible to combine with select (unless forcing no
+            # buffering).
+            if self.universal_newlines and hasattr(file, 'newlines'):
+                if stdout:
+                    stdout = self._translate_newlines(stdout)
+                if stderr:
+                    stderr = self._translate_newlines(stderr)
+
+            self.wait()
+            return (stdout, stderr)
+
+    else:
+        #
+        # POSIX methods
+        #
+        def _get_handles(self, stdin, stdout, stderr):
+            """Construct and return tupel with IO objects:
+            p2cread, p2cwrite, c2pread, c2pwrite, errread, errwrite
+            """
+            p2cread, p2cwrite = None, None
+            c2pread, c2pwrite = None, None
+            errread, errwrite = None, None
+
+            if stdin is None:
+                pass
+            elif stdin == PIPE:
+                p2cread, p2cwrite = os.pipe()
+            elif isinstance(stdin, int):
+                p2cread = stdin
+            else:
+                # Assuming file-like object
+                p2cread = stdin.fileno()
+
+            if stdout is None:
+                pass
+            elif stdout == PIPE:
+                c2pread, c2pwrite = os.pipe()
+            elif isinstance(stdout, int):
+                c2pwrite = stdout
+            else:
+                # Assuming file-like object
+                c2pwrite = stdout.fileno()
+
+            if stderr is None:
+                pass
+            elif stderr == PIPE:
+                errread, errwrite = os.pipe()
+            elif stderr == STDOUT:
+                errwrite = c2pwrite
+            elif isinstance(stderr, int):
+                errwrite = stderr
+            else:
+                # Assuming file-like object
+                errwrite = stderr.fileno()
+
+            return (p2cread, p2cwrite,
+                    c2pread, c2pwrite,
+                    errread, errwrite)
+
+
+        def _set_cloexec_flag(self, fd):
+            try:
+                cloexec_flag = fcntl.FD_CLOEXEC
+            except AttributeError:
+                cloexec_flag = 1
+
+            old = fcntl.fcntl(fd, fcntl.F_GETFD)
+            fcntl.fcntl(fd, fcntl.F_SETFD, old | cloexec_flag)
+
+
+        def _close_fds(self, but):
+            for i in xrange(3, MAXFD):
+                if i == but:
+                    continue
+                try:
+                    os.close(i)
+                except:
+                    pass
+
+
+        def _execute_child(self, args, executable, preexec_fn, close_fds,
+                           cwd, env, universal_newlines,
+                           startupinfo, creationflags, shell,
+                           p2cread, p2cwrite,
+                           c2pread, c2pwrite,
+                           errread, errwrite):
+            """Execute program (POSIX version)"""
+
+            if isinstance(args, types.StringTypes):
+                args = [args]
+
+            if shell:
+                args = ["/bin/sh", "-c"] + args
+
+            if executable is None:
+                executable = args[0]
+
+            # For transferring possible exec failure from child to parent
+            # The first char specifies the exception type: 0 means
+            # OSError, 1 means some other error.
+            errpipe_read, errpipe_write = os.pipe()
+            self._set_cloexec_flag(errpipe_write)
+
+            self.pid = os.fork()
+            self._child_created = True
+            if self.pid == 0:
+                # Child
+                try:
+                    # Close parent's pipe ends
+                    if p2cwrite:
+                        os.close(p2cwrite)
+                    if c2pread:
+                        os.close(c2pread)
+                    if errread:
+                        os.close(errread)
+                    os.close(errpipe_read)
+
+                    # Dup fds for child
+                    if p2cread:
+                        os.dup2(p2cread, 0)
+                    if c2pwrite:
+                        os.dup2(c2pwrite, 1)
+                    if errwrite:
+                        os.dup2(errwrite, 2)
+
+                    # Close pipe fds.  Make sure we don't close the same
+                    # fd more than once, or standard fds.
+                    if p2cread:
+                        os.close(p2cread)
+                    if c2pwrite and c2pwrite not in (p2cread,):
+                        os.close(c2pwrite)
+                    if errwrite and errwrite not in (p2cread, c2pwrite):
+                        os.close(errwrite)
+
+                    # Close all other fds, if asked for
+                    if close_fds:
+                        self._close_fds(but=errpipe_write)
+
+                    if cwd is not None:
+                        os.chdir(cwd)
+
+                    if preexec_fn:
+                        apply(preexec_fn)
+
+                    if env is None:
+                        os.execvp(executable, args)
+                    else:
+                        os.execvpe(executable, args, env)
+
+                except:
+                    exc_type, exc_value, tb = sys.exc_info()
+                    # Save the traceback and attach it to the exception object
+                    exc_lines = traceback.format_exception(exc_type,
+                                                           exc_value,
+                                                           tb)
+                    exc_value.child_traceback = ''.join(exc_lines)
+                    os.write(errpipe_write, pickle.dumps(exc_value))
+
+                # This exitcode won't be reported to applications, so it
+                # really doesn't matter what we return.
+                os._exit(255)
+
+            # Parent
+            os.close(errpipe_write)
+            if p2cread and p2cwrite:
+                os.close(p2cread)
+            if c2pwrite and c2pread:
+                os.close(c2pwrite)
+            if errwrite and errread:
+                os.close(errwrite)
+
+            # Wait for exec to fail or succeed; possibly raising exception
+            data = os.read(errpipe_read, 1048576) # Exceptions limited to 1 MB
+            os.close(errpipe_read)
+            if data != "":
+                os.waitpid(self.pid, 0)
+                child_exception = pickle.loads(data)
+                raise child_exception
+
+
+        def _handle_exitstatus(self, sts):
+            if os.WIFSIGNALED(sts):
+                self.returncode = -os.WTERMSIG(sts)
+            elif os.WIFEXITED(sts):
+                self.returncode = os.WEXITSTATUS(sts)
+            else:
+                # Should never happen
+                raise RuntimeError("Unknown child exit status!")
+
+
+        def poll(self, _deadstate=None):
+            """Check if child process has terminated.  Returns returncode
+            attribute."""
+            if self.returncode is None:
+                try:
+                    pid, sts = os.waitpid(self.pid, os.WNOHANG)
+                    if pid == self.pid:
+                        self._handle_exitstatus(sts)
+                except os.error:
+                    if _deadstate is not None:
+                        self.returncode = _deadstate
+            return self.returncode
+
+
+        def wait(self):
+            """Wait for child process to terminate.  Returns returncode
+            attribute."""
+            if self.returncode is None:
+                pid, sts = os.waitpid(self.pid, 0)
+                self._handle_exitstatus(sts)
+            return self.returncode
+
+
+        def _communicate(self, input):
+            read_set = []
+            write_set = []
+            stdout = None # Return
+            stderr = None # Return
+
+            if self.stdin:
+                # Flush stdio buffer.  This might block, if the user has
+                # been writing to .stdin in an uncontrolled fashion.
+                self.stdin.flush()
+                if input:
+                    write_set.append(self.stdin)
+                else:
+                    self.stdin.close()
+            if self.stdout:
+                read_set.append(self.stdout)
+                stdout = []
+            if self.stderr:
+                read_set.append(self.stderr)
+                stderr = []
+
+            while read_set or write_set:
+                rlist, wlist, xlist = select.select(read_set, write_set, [])
+
+                if self.stdin in wlist:
+                    # When select has indicated that the file is writable,
+                    # we can write up to PIPE_BUF bytes without risk
+                    # blocking.  POSIX defines PIPE_BUF >= 512
+                    bytes_written = os.write(self.stdin.fileno(), input[:512])
+                    input = input[bytes_written:]
+                    if not input:
+                        self.stdin.close()
+                        write_set.remove(self.stdin)
+
+                if self.stdout in rlist:
+                    data = os.read(self.stdout.fileno(), 1024)
+                    if data == "":
+                        self.stdout.close()
+                        read_set.remove(self.stdout)
+                    stdout.append(data)
+
+                if self.stderr in rlist:
+                    data = os.read(self.stderr.fileno(), 1024)
+                    if data == "":
+                        self.stderr.close()
+                        read_set.remove(self.stderr)
+                    stderr.append(data)
+
+            # All data exchanged.  Translate lists into strings.
+            if stdout is not None:
+                stdout = ''.join(stdout)
+            if stderr is not None:
+                stderr = ''.join(stderr)
+
+            # Translate newlines, if requested.  We cannot let the file
+            # object do the translation: It is based on stdio, which is
+            # impossible to combine with select (unless forcing no
+            # buffering).
+            if self.universal_newlines and hasattr(file, 'newlines'):
+                if stdout:
+                    stdout = self._translate_newlines(stdout)
+                if stderr:
+                    stderr = self._translate_newlines(stderr)
+
+            self.wait()
+            return (stdout, stderr)
+
+
+def _demo_posix():
+    #
+    # Example 1: Simple redirection: Get process list
+    #
+    plist = Popen(["ps"], stdout=PIPE).communicate()[0]
+    print "Process list:"
+    print plist
+
+    #
+    # Example 2: Change uid before executing child
+    #
+    if os.getuid() == 0:
+        p = Popen(["id"], preexec_fn=lambda: os.setuid(100))
+        p.wait()
+
+    #
+    # Example 3: Connecting several subprocesses
+    #
+    print "Looking for 'hda'..."
+    p1 = Popen(["dmesg"], stdout=PIPE)
+    p2 = Popen(["grep", "hda"], stdin=p1.stdout, stdout=PIPE)
+    print repr(p2.communicate()[0])
+
+    #
+    # Example 4: Catch execution error
+    #
+    print
+    print "Trying a weird file..."
+    try:
+        print Popen(["/this/path/does/not/exist"]).communicate()
+    except OSError, e:
+        if e.errno == errno.ENOENT:
+            print "The file didn't exist.  I thought so..."
+            print "Child traceback:"
+            print e.child_traceback
+        else:
+            print "Error", e.errno
+    else:
+        print >>sys.stderr, "Gosh.  No error."
+
+
+def _demo_windows():
+    #
+    # Example 1: Connecting several subprocesses
+    #
+    print "Looking for 'PROMPT' in set output..."
+    p1 = Popen("set", stdout=PIPE, shell=True)
+    p2 = Popen('find "PROMPT"', stdin=p1.stdout, stdout=PIPE)
+    print repr(p2.communicate()[0])
+
+    #
+    # Example 2: Simple execution of program
+    #
+    print "Executing calc..."
+    p = Popen("calc")
+    p.wait()
+
+
+if __name__ == "__main__":
+    if mswindows:
+        _demo_windows()
+    else:
+        _demo_posix()
diff --git a/func/minion/utils.py b/func/minion/utils.py
new file mode 100755 (executable)
index 0000000..a7ea788
--- /dev/null
@@ -0,0 +1,207 @@
+"""
+Copyright 2007, Red Hat, Inc
+see AUTHORS
+
+This software may be freely redistributed under the terms of the GNU
+general public license.
+
+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., 675 Mass Ave, Cambridge, MA 02139, USA.
+"""
+
+import os
+import socket
+import string
+import sys
+import time
+import traceback
+import xmlrpclib
+import glob
+import traceback
+
+import codes
+from func import certs
+from func.config import read_config
+from func.commonconfig import FuncdConfig
+from func import logger
+
+# "localhost" is a lame hostname to use for a key, so try to get
+# a more meaningful hostname. We do this by connecting to the certmaster
+# and seeing what interface/ip it uses to make that connection, and looking
+# up the hostname for that. 
+def get_hostname():
+
+    # FIXME: this code ignores http proxies (which granted, we don't
+    #      support elsewhere either. It also hardcodes the port number
+    #      for the certmaster for now
+    hostname = None
+    hostname = socket.gethostname()
+    try:
+        ip = socket.gethostbyname(hostname)
+    except:
+        return hostname
+    if ip != "127.0.0.1":
+        return hostname
+
+
+    config_file = '/etc/func/minion.conf'
+    config = read_config(config_file, FuncdConfig)
+
+    server = config.certmaster
+    port = 51235
+
+    try:
+        s = socket.socket()
+        s.settimeout(5)
+        s.connect((server, port))
+        (intf, port) = s.getsockname()
+        hostname = socket.gethostbyaddr(intf)[0]
+        s.close()
+    except:
+        s.close()
+        raise
+
+    return hostname
+    
+
+
+def create_minion_keys():
+    config_file = '/etc/func/minion.conf'
+    config = read_config(config_file, FuncdConfig)
+    cert_dir = config.cert_dir
+    master_uri = 'http://%s:51235/' % config.certmaster
+    hn = get_hostname()
+
+    if hn is None:
+        raise codes.FuncException("Could not determine a hostname other than localhost")
+
+    key_file = '%s/%s.pem' % (cert_dir, hn)
+    csr_file = '%s/%s.csr' % (cert_dir, hn)
+    cert_file = '%s/%s.cert' % (cert_dir, hn)
+    ca_cert_file = '%s/ca.cert' % cert_dir
+
+
+    if os.path.exists(cert_file) and os.path.exists(ca_cert_file):
+        return
+
+    keypair = None
+    try:
+        if not os.path.exists(cert_dir):
+            os.makedirs(cert_dir)
+        if not os.path.exists(key_file):
+            keypair = certs.make_keypair(dest=key_file)
+        if not os.path.exists(csr_file):
+            if not keypair:
+                keypair = certs.retrieve_key_from_file(key_file)
+            csr = certs.make_csr(keypair, dest=csr_file)
+    except Exception, e:
+        traceback.print_exc()
+        raise codes.FuncException, "Could not create local keypair or csr for minion funcd session"
+
+    result = False
+    log = logger.Logger().logger
+    while not result:
+        try:
+            log.debug("submitting CSR to certmaster %s" % master_uri)
+            result, cert_string, ca_cert_string = submit_csr_to_master(csr_file, master_uri)
+        except socket.gaierror, e:
+            raise codes.FuncException, "Could not locate certmaster at %s" % master_uri
+
+        # logging here would be nice
+        if not result:
+            log.warning("no response from certmaster %s, sleeping 10 seconds" % master_uri)
+            time.sleep(10)
+
+
+    if result:
+        log.debug("received certificate from certmaster %s, storing" % master_uri)
+        cert_fd = os.open(cert_file, os.O_RDWR|os.O_CREAT, 0644)
+        os.write(cert_fd, cert_string)
+        os.close(cert_fd)
+
+        ca_cert_fd = os.open(ca_cert_file, os.O_RDWR|os.O_CREAT, 0644)
+        os.write(ca_cert_fd, ca_cert_string)
+        os.close(ca_cert_fd)
+
+def submit_csr_to_master(csr_file, master_uri):
+    """"
+    gets us our cert back from the certmaster.wait_for_cert() method
+    takes csr_file as path location and master_uri
+    returns Bool, str(cert), str(ca_cert)
+    """
+
+    fo = open(csr_file)
+    csr = fo.read()
+    s = xmlrpclib.ServerProxy(master_uri)
+
+    return s.wait_for_cert(csr)
+
+
+# this is kind of handy, so keep it around for now
+# but we really need to fix out server side logging and error
+# reporting so we don't need it
+def trace_me():
+    x = traceback.extract_stack()
+    bar = string.join(traceback.format_list(x))
+    return bar
+
+
+def daemonize(pidfile=None):
+    """
+    Daemonize this process with the UNIX double-fork trick.
+    Writes the new PID to the provided file name if not None.
+    """
+
+    print pidfile
+    pid = os.fork()
+    if pid > 0:
+        sys.exit(0)
+    os.setsid()
+    os.umask(0)
+    pid = os.fork()
+
+
+    if pid > 0:
+        if pidfile is not None:
+            open(pidfile, "w").write(str(pid))
+        sys.exit(0)
+
+def get_acls_from_config(acldir='/etc/func/minion-acl.d'):
+    """
+    takes a dir of .acl files
+    returns a dict of hostname+hash =  [methods, to, run]
+    
+    """
+    
+    acls = {}
+    if not os.path.exists(acldir):
+        print 'acl dir does not exist: %s' % acldir
+        return acls
+    
+    # get the set of files
+    acl_glob = '%s/*.acl' % acldir
+    files = glob.glob(acl_glob)
+    
+    for acl_file in files:
+        
+        try:
+            fo = open(acl_file, 'r')
+        except (IOError, OSError), e:
+            print 'cannot open acl config file: %s - %s' % (acl_file, e)
+            continue
+    
+        for line in fo.readlines():
+            if line.startswith('#'): continue
+            if line.strip() == '': continue
+            line = line.replace('\n', '')
+            (host, methods) = line.split('=')
+            host = host.strip().lower()
+            methods = methods.strip()
+            methods = methods.replace(',',' ')
+            methods = methods.split()
+            if not acls.has_key(host):
+                acls[host] = []
+            acls[host].extend(methods)
+    
+    return acls
diff --git a/func/overlord/.forkbomb.py.swp b/func/overlord/.forkbomb.py.swp
new file mode 100644 (file)
index 0000000..242b6f4
Binary files /dev/null and b/func/overlord/.forkbomb.py.swp differ
diff --git a/func/overlord/Makefile b/func/overlord/Makefile
new file mode 100755 (executable)
index 0000000..f2bc6c4
--- /dev/null
@@ -0,0 +1,18 @@
+
+
+PYFILES                = $(wildcard *.py)
+
+PYCHECKER       = /usr/bin/pychecker
+PYFLAKES       = /usr/bin/pyflakes
+
+clean::
+       @rm -fv *.pyc *~ .*~ *.pyo
+       @find . -name .\#\* -exec rm -fv {} \;
+       @rm -fv *.rpm
+
+
+pychecker::
+       @$(PYCHECKER) $(PYFILES) || exit 0
+
+pyflakes::
+       @$(PYFLAKES) $(PYFILES) || exit 0
diff --git a/func/overlord/__init__.py b/func/overlord/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/func/overlord/__init__.pyc b/func/overlord/__init__.pyc
new file mode 100644 (file)
index 0000000..f74bc59
Binary files /dev/null and b/func/overlord/__init__.pyc differ
diff --git a/func/overlord/client.py b/func/overlord/client.py
new file mode 100755 (executable)
index 0000000..cf1009c
--- /dev/null
@@ -0,0 +1,336 @@
+##
+## func command line interface & client lib
+##
+## Copyright 2007, Red Hat, Inc
+## Michael DeHaan <mdehaan@redhat.com>
+## +AUTHORS
+##
+## This software may be freely redistributed under the terms of the GNU
+## general public license.
+##
+## 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., 675 Mass Ave, Cambridge, MA 02139, USA.
+##
+
+import sys
+import glob
+import os
+
+from func.commonconfig import CMConfig
+from func.config import read_config, CONFIG_FILE
+
+import sslclient
+
+import command
+import groups
+import func.forkbomb as forkbomb
+import func.jobthing as jobthing
+import func.utils as utils
+from func.CommonErrors import *
+
+# ===================================
+# defaults
+# TO DO: some of this may want to come from config later
+
+DEFAULT_PORT = 51234
+FUNC_USAGE = "Usage: %s [ --help ] [ --verbose ] target.example.org module method arg1 [...]"
+
+# ===================================
+
+class CommandAutomagic(object):
+    """
+    This allows a client object to act as if it were one machine, when in
+    reality it represents many.
+    """
+
+    def __init__(self, clientref, base, nforks=1):
+        self.base = base
+        self.clientref = clientref
+        self.nforks = nforks
+
+    def __getattr__(self,name):
+        base2 = self.base[:]
+        base2.append(name)
+        return CommandAutomagic(self.clientref, base2, self.nforks)
+
+    def __call__(self, *args):
+        if not self.base:
+            raise AttributeError("something wrong here")
+        if len(self.base) < 2:
+            raise AttributeError("no method called: %s" % ".".join(self.base))
+        module = self.base[0]
+        method = ".".join(self.base[1:])
+        return self.clientref.run(module,method,args,nforks=self.nforks)
+
+
+def get_groups():    
+    group_class = groups.Groups()
+    return group_class.get_groups()
+
+
+def get_hosts_by_groupgoo(groups, groupgoo):
+    group_gloobs = groupgoo.split(':')
+    hosts = []
+    for group_gloob in group_gloobs:
+        if not group_gloob[0] == "@":
+            continue
+        if groups.has_key(group_gloob[1:]):
+            hosts = hosts + groups[group_gloob[1:]]
+        else:            
+            print "group %s not defined" % group_gloob
+    return hosts
+
+# ===================================
+# this is a module level def so we can use it and isServer() from
+# other modules with a Client class
+def expand_servers(spec, port=51234, noglobs=None, verbose=None, just_fqdns=False):
+    """
+    Given a regex/blob of servers, expand to a list
+    of server ids.
+    """
+
+
+    # FIXME: we need to refactor expand_servers, it seems to do
+    # weird things, reload the config and groups config everytime it's
+    # called for one, which may or may not be bad... -akl
+    config  = read_config(CONFIG_FILE, CMConfig)
+
+    if noglobs:
+        if not just_fqdns:
+            return [ "https://%s:%s" % (spec, port) ]
+        else:
+            return spec
+
+    group_dict = get_groups()
+
+    all_hosts = []
+    all_certs = []
+    seperate_gloobs = spec.split(";")
+    
+    new_hosts = get_hosts_by_groupgoo(group_dict, spec)
+
+    seperate_gloobs = spec.split(";")
+    seperate_gloobs = seperate_gloobs + new_hosts
+    for each_gloob in seperate_gloobs:
+        actual_gloob = "%s/%s.cert" % (config.certroot, each_gloob)
+        certs = glob.glob(actual_gloob)
+        for cert in certs:
+            all_certs.append(cert)
+            host = cert.replace(config.certroot,"")[1:-5]
+            all_hosts.append(host)
+
+    all_urls = []
+    for x in all_hosts:
+        if not just_fqdns:
+            all_urls.append("https://%s:%s" % (x, port))
+        else:
+            all_urls.append(x)
+
+    if verbose and len(all_urls) == 0:
+        sys.stderr.write("no hosts matched\n")
+
+    return all_urls
+
+
+# does the hostnamegoo actually expand to anything?
+def isServer(server_string):
+    servers = expand_servers(server_string)
+    if len(servers) > 0:
+        return True
+    return False
+
+
+class Client(object):
+
+    def __init__(self, server_spec, port=DEFAULT_PORT, interactive=False,
+        verbose=False, noglobs=False, nforks=1, config=None, async=False, init_ssl=True):
+        """
+        Constructor.
+        @server_spec -- something like "*.example.org" or "foosball"
+        @port -- is the port where all funcd processes should be contacted
+        @verbose -- whether to print unneccessary things
+        @noglobs -- specifies server_spec is not a glob, and run should return single values
+        @config -- optional config object
+        """
+        self.config  = config
+        if config is None:
+            self.config  = read_config(CONFIG_FILE, CMConfig)
+    
+
+        self.server_spec = server_spec
+        self.port        = port
+        self.verbose     = verbose
+        self.interactive = interactive
+        self.noglobs     = noglobs
+        self.nforks      = nforks
+        self.async       = async
+        
+        self.servers  = expand_servers(self.server_spec, port=self.port, noglobs=self.noglobs,verbose=self.verbose)
+    
+        if init_ssl:
+            self.setup_ssl()
+            
+    def setup_ssl(self, client_key=None, client_cert=None, ca=None):
+        # defaults go:
+          # certmaster key, cert, ca
+          # funcd key, cert, ca
+          # raise FuncClientError
+        ol_key = '%s/funcmaster.key' % self.config.cadir
+        ol_crt = '%s/funcmaster.crt' % self.config.cadir
+        myname = utils.get_hostname()
+        # maybe /etc/pki/func is a variable somewhere?
+        fd_key = '/etc/pki/func/%s.pem' % myname
+        fd_crt = '/etc/pki/func/%s.cert' % myname
+        self.ca = '%s/funcmaster.crt' % self.config.cadir
+        if client_key and client_cert and ca:        
+            if (os.access(client_key, os.R_OK) and os.access(client_cert, os.R_OK)
+                            and os.access(ca, os.R_OK)):
+                self.key = client_key
+                self.cert = client_cert
+                self.ca = ca
+        # otherwise fall through our defaults
+        elif os.access(ol_key, os.R_OK) and os.access(ol_crt, os.R_OK):
+            self.key = ol_key
+            self.cert = ol_crt
+        elif os.access(fd_key, os.R_OK) and os.access(fd_crt, os.R_OK):
+            self.key = fd_key
+            self.cert = fd_crt
+        else:
+            raise Func_Client_Exception, 'Cannot read ssl credentials: ssl, cert, ca'
+            
+
+        
+    
+    def __getattr__(self, name):
+        """
+        This getattr allows manipulation of the object as if it were
+        a XMLRPC handle to a single machine, when in reality it is a handle
+        to an unspecified number of machines.
+
+        So, it enables stuff like this:
+
+        Client("*.example.org").yum.install("foo")
+
+        # WARNING: any missing values in Client's source will yield
+        # strange errors with this engaged.  Be aware of that.
+        """
+
+        return CommandAutomagic(self, [name], self.nforks)
+
+    # -----------------------------------------------
+
+    def job_status(self, jobid):
+        """
+        Use this to acquire status from jobs when using run with async client handles
+        """
+        return jobthing.job_status(jobid, client_class=Client)
+
+    # -----------------------------------------------
+
+    def run(self, module, method, args, nforks=1):
+        """
+        Invoke a remote method on one or more servers.
+        Run returns a hash, the keys are server names, the values are the
+        returns.
+
+        The returns may include exception objects.
+        If Client() was constructed with noglobs=True, the return is instead
+        just a single value, not a hash.
+        """
+
+        results = {}
+
+        def process_server(bucketnumber, buckets, server):
+            
+            conn = sslclient.FuncServer(server, self.key, self.cert, self.ca )
+            # conn = xmlrpclib.ServerProxy(server)
+
+            if self.interactive:
+                sys.stderr.write("on %s running %s %s (%s)\n" % (server,
+                    module, method, ",".join(args)))
+
+            # FIXME: support userland command subclassing only if a module
+            # is present, otherwise run as follows.  -- MPD
+
+            try:
+                # thats some pretty code right there aint it? -akl
+                # we can't call "call" on s, since thats a rpc, so
+                # we call gettatr around it.
+                meth = "%s.%s" % (module, method)
+
+                # async calling signature has an "imaginary" prefix
+                # so async.abc.def does abc.def as a background task.
+                # see Wiki docs for details
+                if self.async:
+                    meth = "async.%s" % meth
+
+                # this is the point at which we make the remote call.
+                retval = getattr(conn, meth)(*args[:])
+
+                if self.interactive:
+                    print retval
+            except Exception, e:
+                (t, v, tb) = sys.exc_info()
+                retval = utils.nice_exception(t,v,tb)
+                if self.interactive:
+                    sys.stderr.write("remote exception on %s: %s\n" %
+                        (server, str(e)))
+
+            if self.noglobs:
+                return retval
+            else:
+                left = server.rfind("/")+1
+                right = server.rfind(":")
+                server_name = server[left:right]
+                return (server_name, retval)
+
+        if not self.noglobs:
+            if self.nforks > 1 or self.async:
+                # using forkbomb module to distribute job over multiple threads
+                if not self.async:
+                    results = forkbomb.batch_run(self.servers, process_server, nforks)
+                else:
+                    results = jobthing.batch_run(self.servers, process_server, nforks)
+            else:
+                # no need to go through the fork code, we can do this directly
+                results = {}
+                for x in self.servers:
+                    (nkey,nvalue) = process_server(0, 0, x)
+                    results[nkey] = nvalue    
+        else:
+            # globbing is not being used, but still need to make sure
+            # URI is well formed.
+            expanded = expand_servers(self.server_spec, port=self.port, noglobs=True, verbose=self.verbose)[0]
+            results = process_server(0, 0, expanded)
+
+        return results
+
+   # -----------------------------------------------
+
+    def cli_return(self,results):
+        """
+        As the return code list could return strings and exceptions
+        and all sorts of crazy stuff, reduce it down to a simple
+        integer return.  It may not be useful but we need one.
+        """
+        numbers = []
+        for x in results.keys():
+            # faults are the most important
+            if type(x) == Exception:
+                return -911
+            # then pay attention to numbers
+            if type(x) == int:
+                numbers.append(x)
+
+        # if there were no numbers, assume 0
+        if len(numbers) == 0:
+            return 0
+
+        # if there were numbers, return the highest
+        # (presumably the worst error code
+        max = -9999
+        for x in numbers:
+            if x > max:
+                max = x
+        return max
diff --git a/func/overlord/client.pyc b/func/overlord/client.pyc
new file mode 100644 (file)
index 0000000..5c7874e
Binary files /dev/null and b/func/overlord/client.pyc differ
diff --git a/func/overlord/cmd_modules/__init__.py b/func/overlord/cmd_modules/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/func/overlord/cmd_modules/__init__.pyc b/func/overlord/cmd_modules/__init__.pyc
new file mode 100644 (file)
index 0000000..287b354
Binary files /dev/null and b/func/overlord/cmd_modules/__init__.pyc differ
diff --git a/func/overlord/cmd_modules/call.py b/func/overlord/cmd_modules/call.py
new file mode 100644 (file)
index 0000000..7add5bf
--- /dev/null
@@ -0,0 +1,114 @@
+"""
+call func method invoker
+
+Copyright 2007, Red Hat, Inc
+see AUTHORS
+
+This software may be freely redistributed under the terms of the GNU
+general public license.
+
+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., 675 Mass Ave, Cambridge, MA 02139, USA.
+"""
+
+
+import optparse
+import pprint
+import xmlrpclib
+
+from func.overlord import command
+from func.overlord import client
+
+DEFAULT_PORT = 51234
+DEFAULT_FORKS = 1
+
+class Call(client.command.Command):
+    name = "call"
+    usage = "call module method name arg1 arg2..."
+    def addOptions(self):
+        self.parser.add_option("-v", "--verbose", dest="verbose",
+                               action="store_true")
+        self.parser.add_option("-x", "--xmlrpc", dest="xmlrpc",
+                               help="output return data in XMLRPC format",
+                               action="store_true")
+        self.parser.add_option("", "--raw", dest="rawprint",
+                               help="output return data using Python print",
+                               action="store_true")
+        self.parser.add_option("-j", "--json", dest="json",
+                               help="output return data using JSON",
+                               action="store_true")
+        self.parser.add_option("-p", "--port", dest="port",
+                               default=DEFAULT_PORT)
+        self.parser.add_option("-f", "--forks", dest="forks",
+                               help="how many parallel processes?  (default 1)",
+                               default=DEFAULT_FORKS)
+
+    def handleOptions(self, options):
+        self.options = options
+
+        self.verbose = options.verbose
+        self.port = options.port
+
+        # I'm not really a fan of the "module methodname" approach
+        # but we'll keep it for now -akl
+
+    def parse(self, argv):
+        self.argv = argv
+
+        return command.Command.parse(self, argv)
+        
+
+    def format_return(self, data):
+        """
+        The call module supports multiple output return types, the default is pprint.
+        """
+        
+        if self.options.xmlrpc:
+            return xmlrpclib.dumps((data,""))
+
+        if self.options.json:
+            try:
+                import simplejson
+                return simplejson.dumps(data)
+            except ImportError:
+                print "WARNING: json support not found, install python-simplejson"
+                return data
+
+        if self.options.rawprint:
+            return data
+            
+        return pprint.pformat(data)
+
+    def do(self, args):
+
+        # I'm not really a fan of the "module methodname" approach
+        # but we'll keep it for now -akl
+
+        # I kind of feel like we shouldn't be parsing args here, but I'm
+        # not sure what the write place is -al;
+        self.module           = args[0]
+        if len(args) > 1:
+            self.method       = args[1]
+        else:
+            self.method       = None
+        if len(args) > 2:
+            self.method_args  = args[2:]
+        else:
+            self.method_args  = []
+
+        # this could get weird, sub sub classes might be calling this
+        # this with multiple.parentCommand.parentCommands...
+        # maybe command.py needs a way to set attrs on subCommands?
+        # or some sort of shared datastruct?
+        self.server_spec = self.parentCommand.server_spec
+
+        client_obj = client.Client(self.server_spec,port=self.port,interactive=True,
+            verbose=self.verbose, config=self.config, nforks=self.options.forks)
+        results = client_obj.run(self.module, self.method, self.method_args)
+
+        # TO DO: add multiplexer support
+        # probably as a higher level module.
+
+        # dump the return code stuff atm till we figure out the right place for it
+        return self.format_return(results)
diff --git a/func/overlord/cmd_modules/call.pyc b/func/overlord/cmd_modules/call.pyc
new file mode 100644 (file)
index 0000000..f6c588d
Binary files /dev/null and b/func/overlord/cmd_modules/call.pyc differ
diff --git a/func/overlord/cmd_modules/copyfile.py b/func/overlord/cmd_modules/copyfile.py
new file mode 100644 (file)
index 0000000..295aeab
--- /dev/null
@@ -0,0 +1,73 @@
+"""
+copyfile command line
+
+Copyright 2007, Red Hat, Inc
+see AUTHORS
+
+This software may be freely redistributed under the terms of the GNU
+general public license.
+
+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., 675 Mass Ave, Cambridge, MA 02139, USA.
+"""
+
+
+import optparse
+import os
+import pprint
+import stat
+import xmlrpclib
+
+from func.overlord import command
+from func.overlord import client
+
+DEFAULT_PORT = 51234
+
+class CopyFile(client.command.Command):
+    name = "copyfile"
+    usage = "copy a file to a client"
+
+
+    def addOptions(self):
+        self.parser.add_option("-f", "--file", dest="filename",
+                               action="store")
+        self.parser.add_option("", "--remotepath", dest="remotepath",
+                                action="store")
+        self.parser.add_option("", "--force", dest="force",
+                               action="store_true")
+        self.parser.add_option("-v", "--verbose", dest="verbose",
+                               action="store_true")
+        self.parser.add_option("-p", "--port", dest="port")
+
+    def handleOptions(self, options):
+        self.port = DEFAULT_PORT
+        if self.options.port:
+            self.port = self.options.port
+
+
+    def do(self, args):
+        self.server_spec = self.parentCommand.server_spec
+
+        client_obj = client.Client(self.server_spec,
+                                   port=self.port,
+                                   interactive=False,
+                                   verbose=self.options.verbose,
+                                   config=self.config)
+
+        
+        try:
+            fb = open(self.options.filename, "r").read()
+        except IOError, e:
+            print "Unable to open file: %s: %s" % (self.options.filename, e)
+            return
+
+        st = os.stat(self.options.filename)
+        mode = stat.S_IMODE(st.st_mode)
+        uid = st.st_uid
+        gid = st.st_gid
+
+    
+        data = xmlrpclib.Binary(fb)
+        results = client_obj.run("copyfile", "copyfile", [self.options.remotepath, data,
+                                                          mode, uid, gid])
diff --git a/func/overlord/cmd_modules/listminions.py b/func/overlord/cmd_modules/listminions.py
new file mode 100644 (file)
index 0000000..50c7e24
--- /dev/null
@@ -0,0 +1,51 @@
+"""
+copyfile command line
+
+Copyright 2007, Red Hat, Inc
+see AUTHORS
+
+This software may be freely redistributed under the terms of the GNU
+general public license.
+
+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., 675 Mass Ave, Cambridge, MA 02139, USA.
+"""
+
+
+import optparse
+import os
+
+from func.overlord import command
+from func.overlord import client
+DEFAULT_PORT = 51234
+
+class ListMinions(client.command.Command):
+    name = "list_minions"
+    usage = "show known minions"
+
+    def addOptions(self):
+        self.parser.add_option("-v", "--verbose", dest="verbose",
+                               action="store_true")
+
+    def handleOptions(self, options):
+        self.port = DEFAULT_PORT
+        if options.verbose:
+            self.verbose = self.options.verbose
+    
+    def do(self, args):
+        self.server_spec = self.parentCommand.server_spec
+
+        client_obj = client.Client(self.server_spec,
+                                   port=self.port,
+                                   interactive=False,
+                                   verbose=self.options.verbose,
+                                   config=self.config)
+
+        servers = client_obj.servers
+        print servers
+        for server in servers:
+            # just cause I hate regex'es -akl
+            host = server.split(':')[-2]
+            host = host.split('/')[-1]
+            print host
diff --git a/func/overlord/cmd_modules/ping.py b/func/overlord/cmd_modules/ping.py
new file mode 100644 (file)
index 0000000..f756fd9
--- /dev/null
@@ -0,0 +1,69 @@
+"""
+copyfile command line
+
+Copyright 2007, Red Hat, Inc
+Michael DeHaan <mdehaan@redhat.com>
+also see AUTHORS
+
+This software may be freely redistributed under the terms of the GNU
+general public license.
+
+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., 675 Mass Ave, Cambridge, MA 02139, USA.
+"""
+
+import optparse
+import os
+import pprint
+import stat
+import xmlrpclib
+
+from func.overlord import command
+from func.overlord import client
+
+# FIXME: this really should not be in each sub module.
+DEFAULT_PORT = 51234
+
+
+class Ping(client.command.Command):
+    name = "ping"
+    usage = "see what func minions are up/accessible"
+
+    def addOptions(self):
+        """
+        Not too many options for you!  (Seriously, it's a simple command ... func "*" ping)
+        """
+        # FIXME: verbose and port should be added globally to all sub modules
+        self.parser.add_option("-v", "--verbose", dest="verbose",
+                               action="store_true")
+        self.parser.add_option("-p", "--port", dest="port",
+                               default=DEFAULT_PORT)
+
+    def handleOptions(self, options):
+        """
+        Nothing to do here...
+        """
+        pass
+
+    def do(self, args):
+        self.server_spec = self.parentCommand.server_spec
+
+        # because this is mainly an interactive command, expand the server list and make seperate connections.
+        # to make things look more speedy.
+
+        servers = client.expand_servers(self.server_spec, port=self.options.port, noglobs=None, 
+                                        verbose=self.options.verbose, just_fqdns=True)
+
+        for server in servers:
+
+            client_obj = client.Client(server,port=self.options.port,interactive=False,
+                                       verbose=self.options.verbose,config=self.config, noglobs=True)
+
+            results = client_obj.run("test", "ping", [])
+            if results == 1:
+                print "[ ok ... ] %s" % server
+            else:
+                print "[ FAILED ] %s" % server
+
+        return 1
diff --git a/func/overlord/cmd_modules/show.py b/func/overlord/cmd_modules/show.py
new file mode 100644 (file)
index 0000000..e1df554
--- /dev/null
@@ -0,0 +1,99 @@
+"""
+show introspection commandline
+
+Copyright 2007, Red Hat, Inc
+see AUTHORS
+
+This software may be freely redistributed under the terms of the GNU
+general public license.
+
+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., 675 Mass Ave, Cambridge, MA 02139, USA.
+"""
+
+
+import optparse
+import pprint
+import xmlrpclib
+
+from func.overlord import command
+from func.overlord import client
+
+DEFAULT_PORT = 51234
+
+
+class ShowHardware(client.command.Command):
+    name = "hardware"
+    usage = "show hardware details"
+
+    # FIXME: we might as well make verbose be in the subclass
+    #      and probably an inc variable while we are at it
+    def addOptions(self):
+        self.parser.add_option("-v", "--verbose", dest="verbose",
+                               action="store_true")
+        self.parser.add_option("-p", "--port", dest="port")
+
+
+    def handleOptions(self, options):
+        self.port = DEFAULT_PORT
+        if self.options.port:
+            self.port = self.options.port
+
+    def parse(self, argv):
+        self.argv = argv
+        return command.Command.parse(self,argv)
+
+    def do(self,args):
+
+        self.server_spec = self.parentCommand.parentCommand.server_spec
+        
+        client_obj = client.Client(self.server_spec,
+                                   port=self.port,
+                                   interactive=False,
+                                   verbose=self.options.verbose,
+                                   config=self.config)
+        
+        results = client_obj.run("hardware", "info", [])
+
+        # if the user 
+        top_options = ["port","verbose"]
+        
+        for minion in results:
+            print "%s:" % minion
+            minion_data = results[minion]
+            # if user set no args
+            if not args:
+                pprint.pprint(minion_data)
+                continue
+            
+            for arg in args:
+                if arg in minion_data:
+                    print minion_data[arg]
+
+
+class Show(client.command.Command):
+    name = "show"
+    usage = "various simple report stuff"
+    subCommandClasses = [ShowHardware]
+    def addOptions(self):
+        self.parser.add_option("-v", "--verbose", dest="verbose",
+                               action="store_true")
+        self.parser.add_option("-p", "--port", dest="port",
+                               default=DEFAULT_PORT)
+
+    def handleOptions(self, options):
+        self.options = options
+
+        self.verbose = options.verbose
+        self.port = options.port
+
+
+    def parse(self, argv):
+        self.argv = argv
+
+        return command.Command.parse(self, argv)
+        
+
+    def do(self, args):
+        pass
diff --git a/func/overlord/command.py b/func/overlord/command.py
new file mode 100644 (file)
index 0000000..7fb7de4
--- /dev/null
@@ -0,0 +1,287 @@
+# -*- Mode: Python; test-case-name: test_command -*-
+# vi:si:et:sw=4:sts=4:ts=4
+
+# This file is released under the standard PSF license.
+#
+#  from MOAP - https://thomas.apestaart.org/moap/trac
+#    written by Thomas Vander Stichele (thomas at apestaart dot org)
+#
+
+"""
+Command class.
+"""
+
+import optparse
+import sys
+
+from func.config import read_config, CONFIG_FILE
+from func.commonconfig import CMConfig
+
+class CommandHelpFormatter(optparse.IndentedHelpFormatter):
+    """
+    I format the description as usual, but add an overview of commands
+    after it if there are any, formatted like the options.
+    """
+    _commands = None
+
+    def addCommand(self, name, description):
+        if self._commands is None:
+            self._commands = {}
+        self._commands[name] = description
+
+    ### override parent method
+    def format_description(self, description):
+        # textwrap doesn't allow for a way to preserve double newlines
+        # to separate paragraphs, so we do it here.
+        blocks = description.split('\n\n')
+        rets = []
+
+        for block in blocks:
+            rets.append(optparse.IndentedHelpFormatter.format_description(self,
+                block))
+        ret = "\n".join(rets)
+        if self._commands:
+            commandDesc = []
+            commandDesc.append("commands:")
+            keys = self._commands.keys()
+            keys.sort()
+            length = 0
+            for key in keys:
+                if len(key) > length:
+                    length = len(key)
+            for name in keys:
+                format = "  %-" + "%d" % length + "s  %s"
+                commandDesc.append(format % (name, self._commands[name]))
+            ret += "\n" + "\n".join(commandDesc) + "\n"
+        return ret
+
+class CommandOptionParser(optparse.OptionParser):
+    """
+    I parse options as usual, but I explicitly allow setting stdout
+    so that our print_help() method (invoked by default with -h/--help)
+    defaults to writing there.
+    """
+    _stdout = sys.stdout
+
+    def set_stdout(self, stdout):
+        self._stdout = stdout
+
+    # we're overriding the built-in file, but we need to since this is
+    # the signature from the base class
+    __pychecker__ = 'no-shadowbuiltin'
+    def print_help(self, file=None):
+        # we are overriding a parent method so we can't do anything about file
+        __pychecker__ = 'no-shadowbuiltin'
+        if file is None:
+            file = self._stdout
+        file.write(self.format_help())
+
+class Command:
+    """
+    I am a class that handles a command for a program.
+    Commands can be nested underneath a command for further processing.
+
+    @cvar name:        name of the command, lowercase
+    @cvar aliases:     list of alternative lowercase names recognized
+    @type aliases:     list of str
+    @cvar usage:       short one-line usage string;
+                       %command gets expanded to a sub-command or [commands]
+                       as appropriate
+    @cvar summary:     short one-line summary of the command
+    @cvar description: longer paragraph explaining the command
+    @cvar subCommands: dict of name -> commands below this command
+    @type subCommands: dict of str  -> L{Command}
+    """
+    name = None
+    aliases = None
+    usage = None
+    summary = None
+    description = None
+    parentCommand = None
+    subCommands = None
+    subCommandClasses = None
+    aliasedSubCommands = None
+
+    def __init__(self, parentCommand=None, stdout=sys.stdout,
+        stderr=sys.stderr):
+        """
+        Create a new command instance, with the given parent.
+        Allows for redirecting stdout and stderr if needed.
+        This redirection will be passed on to child commands.
+        """
+        if not self.name:
+            self.name = str(self.__class__).split('.')[-1].lower()
+        self.stdout = stdout
+        self.stderr = stderr
+        self.parentCommand = parentCommand
+
+        self.config = read_config(CONFIG_FILE, CMConfig)
+
+        # create subcommands if we have them
+        self.subCommands = {}
+        self.aliasedSubCommands = {}
+        if self.subCommandClasses:
+            for C in self.subCommandClasses:
+                c = C(self, stdout=stdout, stderr=stderr)
+                self.subCommands[c.name] = c
+                if c.aliases:
+                    for alias in c.aliases:
+                        self.aliasedSubCommands[alias] = c
+
+        # create our formatter and add subcommands if we have them
+        formatter = CommandHelpFormatter()
+        if self.subCommands:
+            for name, command in self.subCommands.items():
+                formatter.addCommand(name, command.summary or
+                    command.description)
+
+        # expand %command for the bottom usage
+        usage = self.usage or self.name
+        if usage.find("%command") > -1:
+            usage = usage.split("%command")[0] + '[command]'
+        usages = [usage, ]
+
+        # FIXME: abstract this into getUsage that takes an optional
+        # parentCommand on where to stop recursing up
+        # useful for implementing subshells
+
+        # walk the tree up for our usage
+        c = self.parentCommand
+        while c:
+            usage = c.usage or c.name
+            if usage.find(" %command") > -1:
+                usage = usage.split(" %command")[0]
+            usages.append(usage)
+            c = c.parentCommand
+        usages.reverse()
+        usage = " ".join(usages)
+
+        # create our parser
+        description = self.description or self.summary
+        self.parser = CommandOptionParser(
+            usage=usage, description=description,
+            formatter=formatter)
+        self.parser.set_stdout(self.stdout)
+        self.parser.disable_interspersed_args()
+
+        # allow subclasses to add options
+        self.addOptions()
+
+    def addOptions(self):
+        """
+        Override me to add options to the parser.
+        """
+        pass
+
+    def do(self, args):
+        """
+        Override me to implement the functionality of the command.
+        """
+        pass
+
+    def parse(self, argv):
+        """
+        Parse the given arguments and act on them.
+
+        @rtype:   int
+        @returns: an exit code
+        """
+        self.options, args = self.parser.parse_args(argv)
+
+        # FIXME: make handleOptions not take options, since we store it
+        # in self.options now
+        ret = self.handleOptions(self.options)
+        if ret:
+            return ret
+
+        # handle pleas for help
+        if args and args[0] == 'help':
+            self.debug('Asked for help, args %r' % args)
+
+            # give help on current command if only 'help' is passed
+            if len(args) == 1:
+                self.outputHelp()
+                return 0
+
+            # complain if we were asked for help on a subcommand, but we don't
+            # have any
+            if not self.subCommands:
+                self.stderr.write('No subcommands defined.')
+                self.parser.print_usage(file=self.stderr)
+                self.stderr.write(
+                    "Use --help to get more information about this command.\n")
+                return 1
+
+            # rewrite the args the other way around;
+            # help doap becomes doap help so it gets deferred to the doap
+            # command
+            args = [args[1], args[0]]
+
+
+        # if we have args that we need to deal with, do it now
+        # before we start looking for subcommands
+        self.handleArguments(args)
+
+        # if we don't have subcommands, defer to our do() method
+        if not self.subCommands:
+            ret = self.do(args)
+
+            # if everything's fine, we return 0
+            if not ret:
+                ret = 0
+
+            return ret
+
+
+        # if we do have subcommands, defer to them
+        try:
+            command = args[0]
+        except IndexError:
+            self.parser.print_usage(file=self.stderr)
+            self.stderr.write(
+                "Use --help to get a list of commands.\n")
+            return 1
+
+        if command in self.subCommands.keys():
+            return self.subCommands[command].parse(args[1:])
+
+        if self.aliasedSubCommands:
+            if command in self.aliasedSubCommands.keys():
+                return self.aliasedSubCommands[command].parse(args[1:])
+
+        self.stderr.write("Unknown command '%s'.\n" % command)
+        return 1
+
+    def outputHelp(self):
+        """
+        Output help information.
+        """
+        self.parser.print_help(file=self.stderr)
+
+    def outputUsage(self):
+        """
+        Output usage information.
+        Used when the options or arguments were missing or wrong.
+        """
+        self.parser.print_usage(file=self.stderr)
+
+    def handleOptions(self, options):
+        """
+        Handle the parsed options.
+        """
+        pass
+
+    def handleArguments(self, arguments):
+        """
+        Handle the parsed arguments.
+        """
+        pass
+
+    def getRootCommand(self):
+        """
+        Return the top-level command, which is typically the program.
+        """
+        c = self
+        while c.parentCommand:
+            c = c.parentCommand
+        return c
diff --git a/func/overlord/command.pyc b/func/overlord/command.pyc
new file mode 100644 (file)
index 0000000..6fa44e8
Binary files /dev/null and b/func/overlord/command.pyc differ
diff --git a/func/overlord/forkbomb.pyc b/func/overlord/forkbomb.pyc
new file mode 100644 (file)
index 0000000..308557d
Binary files /dev/null and b/func/overlord/forkbomb.pyc differ
diff --git a/func/overlord/func_command.py b/func/overlord/func_command.py
new file mode 100644 (file)
index 0000000..4cec8a0
--- /dev/null
@@ -0,0 +1,71 @@
+#!/usr/bin/python
+
+## func command line interface & client lib
+##
+## Copyright 2007,2008 Red Hat, Inc
+## +AUTHORS
+##
+## This software may be freely redistributed under the terms of the GNU
+## general public license.
+##
+## 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., 675 Mass Ave, Cambridge, MA 02139, USA.
+
+import sys
+
+
+import command
+
+#FIXME: need a plug-in runtime module loader here
+from cmd_modules import call
+from cmd_modules import show
+from cmd_modules import copyfile
+from cmd_modules import listminions
+from cmd_modules import ping
+
+from func.overlord import client
+
+class FuncCommandLine(command.Command):
+    name = "func"
+    usage = "func is the commandline interface to a func minion"
+
+    subCommandClasses = [call.Call, show.Show,
+                         copyfile.CopyFile, listminions.ListMinions, ping.Ping]
+
+    def __init__(self):
+
+        command.Command.__init__(self)
+
+    def do(self, args):
+        pass
+
+    def addOptions(self):
+        self.parser.add_option('', '--version', action="store_true",
+            help="show version information")
+
+    # just some ugly goo to try to guess if arg[1] is hostnamegoo or
+    # a command name
+    def _isGlob(self, str):
+        if str.find("*") or str.find("?") or str.find("[") or str.find("]"):
+            return True
+        return False
+        
+    def handleArguments(self, args):
+        if len(args) < 2:
+            print "see the func manpage for usage"
+            sys.exit(411)
+        server_string = args[0]
+        # try to be clever about this for now
+        if client.isServer(server_string) or self._isGlob(server_string):
+            self.server_spec = server_string
+            args.pop(0)
+        # if it doesn't look like server, assume it
+        # is a sub command? that seems wrong, what about
+        # typo's and such? How to catch that? -akl
+        # maybe a class variable self.data on Command?
+
+    def handleOptions(self, options):
+        if options.version:
+            #FIXME
+            print "version is NOT IMPLEMENTED YET"
diff --git a/func/overlord/func_command.pyc b/func/overlord/func_command.pyc
new file mode 100644 (file)
index 0000000..1834e0e
Binary files /dev/null and b/func/overlord/func_command.pyc differ
diff --git a/func/overlord/groups.py b/func/overlord/groups.py
new file mode 100644 (file)
index 0000000..8eaf28e
--- /dev/null
@@ -0,0 +1,95 @@
+#!/usr/bin/python
+
+## func command line interface & client lib
+##
+## Copyright 2007,2008 Red Hat, Inc
+## Adrian Likins <alikins@redhat.com>
+## +AUTHORS
+##
+## This software may be freely redistributed under the terms of the GNU
+## general public license.
+##
+## 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., 675 Mass Ave, Cambridge, MA 02139, USA.
+##
+
+
+# this module lets you define groups of systems to work with from the
+# commandline. It uses an "ini" style config parser like:
+
+#[groupname]
+#host = foobar, baz, blip
+#subgroup = blippy
+
+
+import ConfigParser
+import os
+
+
+class Groups(object):
+
+    def __init__(self, filename="/etc/func/groups"):
+        self.filename = filename
+        self.group_names = {}
+        self.groups = {}
+        self.__parse()
+
+    def __parse(self):
+        
+        self.cp = ConfigParser.SafeConfigParser()
+        self.cp.read(self.filename)
+
+        for section in self.cp.sections():
+            self.add_group(section)
+            options = self.cp.options(section)
+            for option in options:
+                if option == "host":
+                    self.add_hosts_to_group(section, self.cp.get(section, option))
+                if option == "subgroup":
+                    pass
+        
+    def show(self):
+        print self.cp.sections()
+        print self.groups
+
+    def add_group(self, group):
+        pass
+
+    def __parse_hoststrings(self, hoststring):
+        hosts = []
+        bits = hoststring.split(';')
+        for bit in bits:
+            blip = bit.strip().split(' ')
+            for host in blip:
+                if host not in hosts:
+                    hosts.append(host.strip())
+
+        return hosts
+
+    def add_hosts_to_group(self, group, hoststring):
+        hosts = self.__parse_hoststrings(hoststring)
+        for host in hosts:
+            self.add_host_to_group(group, host)
+
+        
+
+    def add_host_to_group(self, group, host):
+        if not self.groups.has_key(group):
+            self.groups[group] = []
+        self.groups[group].append(host) 
+
+    def get_groups(self):
+        return self.groups
+
+
+
+def main():
+    g = Groups("/tmp/testgroups")
+    print g.show()
+    
+
+
+if __name__ == "__main__":
+    main()
diff --git a/func/overlord/groups.pyc b/func/overlord/groups.pyc
new file mode 100644 (file)
index 0000000..9ed2a92
Binary files /dev/null and b/func/overlord/groups.pyc differ
diff --git a/func/overlord/highlevel.py b/func/overlord/highlevel.py
new file mode 100644 (file)
index 0000000..977dcb4
--- /dev/null
@@ -0,0 +1,40 @@
+##
+## func higher level API interface for overlord side operations
+##
+## Copyright 2007, Red Hat, Inc
+## Michael DeHaan <mdehaan@redhat.com>
+## +AUTHORS
+##
+## This software may be freely redistributed under the terms of the GNU
+## general public license.
+##
+## 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., 675 Mass Ave, Cambridge, MA 02139, USA.
+##
+
+import exceptions
+
+class HigherLevelObject:
+
+   def __init__(self, client):
+       self.client = client_handle
+
+   def modify(self, key, properties):
+       """
+       Modify or create an entity named key.  
+       properties should contain all neccessary fields.
+       """
+       raise exceptions.NotImplementedError
+
+   def remove(self, key):
+       """
+       Remove an entity named key.
+       """
+       raise exceptions.NotImplementedError
+
+   def list(self):
+       """
+       List all objects
+       """
+       raise exceptions.NotImplementedError  
diff --git a/func/overlord/inventory.py b/func/overlord/inventory.py
new file mode 100755 (executable)
index 0000000..8302a1c
--- /dev/null
@@ -0,0 +1,191 @@
+##
+## func inventory app.
+## use func to collect inventory data on anything, yes, anything
+##
+## Copyright 2007, Red Hat, Inc
+## Michael DeHaan <mdehaan@redhat.com>
+## +AUTHORS
+##
+## This software may be freely redistributed under the terms of the GNU
+## general public license.
+##
+## 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., 675 Mass Ave, Cambridge, MA 02139, USA.
+##
+
+import os.path
+import time
+import optparse
+import sys
+import pprint
+import xmlrpclib
+from func.minion import sub_process
+import func.overlord.client as func_client
+import func.utils as utils
+
+DEFAULT_TREE = "/var/lib/func/inventory/"
+
+
+class FuncInventory(object):
+
+    def __init__(self):
+        pass 
+
+    def run(self,args): 
+
+        p = optparse.OptionParser()
+        p.add_option("-v", "--verbose",
+                     dest="verbose",
+                     action="store_true",
+                     help="provide extra output")
+        p.add_option("-s", "--server-spec",
+                     dest="server_spec",
+                     default="*",
+                     help="run against specific servers, default: '*'")
+        p.add_option("-m", "--methods",
+                     dest="methods",
+                     default="inventory",
+                     help="run inventory only on certain function names, default: 'inventory'")
+        p.add_option("-M", "--modules",
+                     dest="modules",
+                     default="all",
+                     help="run inventory only on certain module names, default: 'all'")
+        p.add_option("-t", "--tree",
+                     dest="tree",
+                     default=DEFAULT_TREE,
+                     help="output results tree here, default: %s" % DEFAULT_TREE)
+        p.add_option("-n", "--no-git",
+                     dest="nogit",
+                     action="store_true",
+                     help="disable useful change tracking features")
+        p.add_option("-x", "--xmlrpc", dest="xmlrpc",
+                     help="output data using XMLRPC format",
+                     action="store_true")
+        p.add_option("-j", "--json", dest="json",
+                     help="output data using JSON",
+                     action="store_true")
+
+
+        (options, args) = p.parse_args(args)
+        self.options = options
+
+        filtered_module_list = options.modules.split(",")
+        filtered_function_list = options.methods.split(",")
+
+        self.git_setup(options)
+
+        # see what modules each host provides (as well as what hosts we have)
+        host_methods = func_client.Client(options.server_spec).system.list_methods()
+       
+        # call all remote info methods and handle them
+        if options.verbose:
+            print "- scanning ..."
+        # for (host, modules) in host_modules.iteritems():
+
+        for (host, methods) in host_methods.iteritems():
+
+            if utils.is_error(methods):
+                print "-- connection refused: %s" % host 
+                break 
+
+            for each_method in methods:
+
+                #if type(each_method) == int:
+                #    if self.options.verbose:
+                #        print "-- connection refused: %s" % host
+                #    break
+
+                tokens = each_method.split(".")
+                module_name = ".".join(tokens[:-1])
+                method_name = tokens[-1]
+
+                if not "all" in filtered_module_list and not module_name in filtered_module_list:
+                    continue
+
+                if not "all" in filtered_function_list and not method_name in filtered_function_list:
+                    continue
+               
+                client = func_client.Client(host,noglobs=True) # ,noglobs=True)
+                results = getattr(getattr(client,module_name),method_name)()
+                if self.options.verbose:
+                    print "-- %s: running: %s %s" % (host, module_name, method_name)
+                self.save_results(options, host, module_name, method_name, results)
+        self.git_update(options)
+        return 1
+
+    def format_return(self, data):
+        """
+        The call module supports multiple output return types, the default is pprint.
+        """
+
+        # special case... if the return is a string, just print it straight
+        if type(data) == str:
+            return data
+
+        if self.options.xmlrpc:
+            return xmlrpclib.dumps((data,""))
+
+        if self.options.json:
+            try:
+                import simplejson
+                return simplejson.dumps(data)
+            except ImportError:
+                print "ERROR: json support not found, install python-simplejson"
+                sys.exit(1)
+
+        return pprint.pformat(data)
+
+    # FUTURE: skvidal points out that guest symlinking would be an interesting feature       
+
+    def save_results(self, options, host_name, module_name, method_name, results):
+        dirname = os.path.join(options.tree, host_name, module_name)
+        if not os.path.exists(dirname):
+             os.makedirs(dirname)
+        filename = os.path.join(dirname, method_name)
+        results_file = open(filename,"w+")
+        data = self.format_return(results)
+        results_file.write(data)
+        results_file.close()
+
+    def git_setup(self,options):
+        if options.nogit:
+            return  
+        if not os.path.exists("/usr/bin/git"):
+            print "git-core is not installed, so no change tracking is available."
+            print "use --no-git or, better, just install it."
+            sys.exit(411) 
+            
+        if not os.path.exists(options.tree):
+            os.makedirs(options.tree)
+        dirname = os.path.join(options.tree, ".git")
+        if not os.path.exists(dirname):
+            if options.verbose:
+                print "- initializing git repo: %s" % options.tree
+            cwd = os.getcwd()
+            os.chdir(options.tree)
+            rc1 = sub_process.call(["/usr/bin/git", "init"], shell=False)
+            # FIXME: check rc's
+            os.chdir(cwd)
+        else:
+            if options.verbose:
+                print "- git already initialized: %s" % options.tree
+
+    def git_update(self,options):
+        if options.nogit:
+            return
+        else:
+            if options.verbose:
+               print "- updating git"
+        mytime = time.asctime()
+        cwd = os.getcwd()
+        os.chdir(options.tree)
+        rc1 = sub_process.call(["/usr/bin/git", "add", "*" ], shell=False)
+        rc2 = sub_process.call(["/usr/bin/git", "commit", "-a", "-m", "Func-inventory update: %s" % mytime], shell=False)
+        # FIXME: check rc's
+        os.chdir(cwd)
+
+
+if __name__ == "__main__":
+    inv = FuncInventory()
+    inv.run(sys.argv)
diff --git a/func/overlord/jobthing.pyc b/func/overlord/jobthing.pyc
new file mode 100644 (file)
index 0000000..cba36cb
Binary files /dev/null and b/func/overlord/jobthing.pyc differ
diff --git a/func/overlord/modules/netapp.py b/func/overlord/modules/netapp.py
new file mode 100644 (file)
index 0000000..987901e
--- /dev/null
@@ -0,0 +1,82 @@
+##
+## Overlord library to interface with minion-side netapp operations
+##
+## Most of this is just wrappers to create some cleaner, earier to use
+## interfaces.  Also allows users to get function signatures and use
+## nice things like kwargs client side, for those of us who can't live
+## without ipython introspection.
+##
+## Copyright 2008, Red Hat, Inc
+## John Eckersberg <jeckersb@redhat.com>
+##
+## This software may be freely redistributed under the terms of the GNU
+## general public license.
+##
+## 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., 675 Mass Ave, Cambridge, MA 02139, USA.
+##
+
+from func.overlord.client import Client
+
+class RemoteError(Exception): pass
+
+def _(res):
+    if type(res) == type([]) and res[0] == 'REMOTE_ERROR':
+        raise RemoteError, res[2]
+    else:
+        return res
+
+class Filer(Client):
+    def __init__(self, filer, admin_host):
+        Client.__init__(self, admin_host)
+        self.filer = filer
+        self.admin_host = admin_host
+
+    def create_volume(self, vol, aggr, size):
+        return _(self.netapp.vol.create(self.filer, vol, aggr, size)[self.admin_host])
+
+    def destroy_volume(self, vol):
+        # offline it first
+        try:
+            self.netapp.vol.offline(self.filer, vol)
+        except:
+            pass
+        return _(self.netapp.vol.destroy(self.filer, vol)[self.admin_host])
+
+    def offline_volume(self, vol):
+        return _(self.netapp.vol.offline(self.filer, vol)[self.admin_host])                 
+
+    def online_volume(self, vol):
+        return _(self.netapp.vol.online(self.filer, vol)[self.admin_host])                 
+
+    def get_volume_size(self, vol):
+        return _(self.netapp.vol.size(self.filer, vol)[self.admin_host])
+
+    def resize_volume(self, vol, delta):
+        return _(self.netapp.vol.size(self.filer, vol, delta)[self.admin_host])
+
+    def create_snapshot(self, vol, snap):
+        return _(self.netapp.snap.create(self.filer, vol, snap)[self.admin_host])
+
+    def delete_snapshot(self, vol, snap):
+        return _(self.netapp.snap.delete(self.filer, vol, snap)[self.admin_host])
+
+    def create_clone_volume(self, vol, parent, snap):
+        return _(self.netapp.vol.clone.create(self.filer, vol, parent, snap)[self.admin_host])
+
+    def split_clone_volume(self, vol):
+        return _(self.netapp.vol.clone.split(self.filer, vol)[self.admin_host])
+
+    def list_volumes(self):
+        vols = _(self.netapp.vol.status(self.filer))
+        return_list = []
+        for vol in vols:
+            return_list.append(vol['name'])
+        return return_list
+
+    def volume_details(self, vol=None):
+        if vol:
+            return _(self.netapp.vol.status(self.filer, vol)[self.admin_host])
+        else:
+            return _(self.netapp.vol.status(self.filer)[self.admin_host])
diff --git a/func/overlord/sslclient.py b/func/overlord/sslclient.py
new file mode 100755 (executable)
index 0000000..3861bb8
--- /dev/null
@@ -0,0 +1,50 @@
+import sys
+import xmlrpclib
+import urllib
+
+from func import SSLCommon
+
+
+class SSL_Transport(xmlrpclib.Transport):
+
+    user_agent = "pyOpenSSL_XMLRPC/%s - %s" % ('0.1', xmlrpclib.Transport.user_agent)
+
+    def __init__(self, ssl_context, timeout=None, use_datetime=0):
+        if sys.version_info[:3] >= (2, 5, 0):
+            xmlrpclib.Transport.__init__(self, use_datetime)
+        self.ssl_ctx=ssl_context
+        self._timeout = timeout
+
+    def make_connection(self, host):
+        # Handle username and password.
+        try:
+            host, extra_headers, x509 = self.get_host_info(host)
+        except AttributeError:
+            # Yay for Python 2.2
+            pass
+        _host, _port = urllib.splitport(host)
+        return SSLCommon.HTTPS(_host, int(_port), ssl_context=self.ssl_ctx, timeout=self._timeout)
+
+
+class SSLXMLRPCServerProxy(xmlrpclib.ServerProxy):
+    def __init__(self, uri, pkey_file, cert_file, ca_cert_file, timeout=None):
+        self.ctx = SSLCommon.CreateSSLContext(pkey_file, cert_file, ca_cert_file)
+        xmlrpclib.ServerProxy.__init__(self, uri, SSL_Transport(ssl_context=self.ctx, timeout=timeout))
+
+
+class FuncServer(SSLXMLRPCServerProxy):
+    def __init__(self, uri, pem=None, crt=None, ca=None):
+        self.pem = pem
+        self.crt = crt
+        self.ca = ca
+
+        SSLXMLRPCServerProxy.__init__(self, uri,
+                                      self.pem,
+                                      self.crt,
+                                      self.ca)
+
+
+if __name__ == "__main__":
+    s = SSLXMLRPCServerProxy('https://localhost:51234/', '/etc/pki/func/slave.pem', '/etc/pki/func/slave.cert', '/etc/pki/func/ca/funcmaster.crt')
+    f = s.ping(1, 2)
+    print f
diff --git a/func/overlord/sslclient.pyc b/func/overlord/sslclient.pyc
new file mode 100644 (file)
index 0000000..fdc21f2
Binary files /dev/null and b/func/overlord/sslclient.pyc differ
diff --git a/func/overlord/test_func.py b/func/overlord/test_func.py
new file mode 100755 (executable)
index 0000000..2b3f041
--- /dev/null
@@ -0,0 +1,61 @@
+#!/usr/bin/python
+
+
+# FIXME: should import the client lib, not XMLRPC lib, when we are done
+
+import xmlrpclib
+import sys
+
+TEST_GETATTR = True
+TEST_PROCESS = False
+TEST_VIRT = False
+TEST_SERVICES = False
+TEST_HARDWARE =  False
+TEST_SMART = True
+
+if TEST_GETATTR:
+    import func.overlord.client as func_client
+    print func_client.Client("*").hardware.pci_info()
+    #print func_client.Client("*").test.add(1,2)
+    #print func_client.Client("*").hardware.info()
+    #print func_client.Client("*").run("hardware","info",[])
+    #print func_client.Client(socket.gethostname(),noglobs=True).test.add("1","2")
+    sys.exit(1)
+
+# get a connecton (to be replaced by client lib logic)
+s = xmlrpclib.ServerProxy("http://127.0.0.1:51234")
+
+# here's the basic test...
+print s.test.add(1, 2)
+
+if TEST_SMART:
+    print s.smart.info()
+
+if TEST_PROCESS:
+    print s.process.info()
+    # print s.process.pkill("thunderbird")
+
+# here's the service module testing
+if TEST_SERVICES:
+    print s.service.restart("httpd")
+
+if TEST_HARDWARE:
+    print s.hardware.info()
+
+# this is so I can remember how the virt module works
+if TEST_VIRT:
+
+    # example of using koan to install a virtual machine
+    #s.virt_install("mdehaan.rdu.redhat.com","profileX")
+
+    # wait ...
+    vms = s.virt.list_vms()
+    # example of stopping all stopped virtual machines
+    print "list of virtual instances = %s" % vms
+    for vm in vms:
+        status = s.virt.status(vm)
+        print status
+        if status == "shutdown":
+            s.virt.start(vm)
+
+# add more tests here
diff --git a/func/utils.py b/func/utils.py
new file mode 100755 (executable)
index 0000000..54c9c39
--- /dev/null
@@ -0,0 +1,73 @@
+"""
+Copyright 2007, Red Hat, Inc
+see AUTHORS
+
+This software may be freely redistributed under the terms of the GNU
+general public license.
+
+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., 675 Mass Ave, Cambridge, MA 02139, USA.
+"""
+
+import os
+import string
+import sys
+import traceback
+import xmlrpclib
+import socket
+
+REMOTE_ERROR = "REMOTE_ERROR"
+
+def trace_me():
+    x = traceback.extract_stack()
+    bar = string.join(traceback.format_list(x))
+    return bar
+
+def daemonize(pidfile=None):
+    """
+    Daemonize this process with the UNIX double-fork trick.
+    Writes the new PID to the provided file name if not None.
+    """
+
+    print pidfile
+    pid = os.fork()
+    if pid > 0:
+        sys.exit(0)
+    os.setsid()
+    os.umask(0)
+    pid = os.fork()
+
+    if pid > 0:
+        if pidfile is not None:
+            open(pidfile, "w").write(str(pid))
+        sys.exit(0)
+
+def nice_exception(etype, evalue, etb):
+    etype = str(etype)
+    lefti = etype.index("'") + 1
+    righti = etype.rindex("'")
+    nicetype = etype[lefti:righti]
+    nicestack = string.join(traceback.format_list(traceback.extract_tb(etb)))
+    return [ REMOTE_ERROR, nicetype, str(evalue), nicestack ] 
+
+def get_hostname():
+    fqdn = socket.getfqdn()
+    host = socket.gethostname()
+    if fqdn.find(host) != -1:
+        return fqdn
+    else:
+        return host
+
+
+def is_error(result):
+    if type(result) != list:
+        return False
+    if len(result) == 0:
+        return False
+    if result[0] == REMOTE_ERROR:
+        return True
+    return False
+
+
+              
diff --git a/init-scripts/certmaster b/init-scripts/certmaster
new file mode 100755 (executable)
index 0000000..819ba0d
--- /dev/null
@@ -0,0 +1,112 @@
+#!/bin/sh
+#
+# certmaster    certmaster
+###################################
+
+# LSB header
+
+### BEGIN INIT INFO
+# Provides: certmaster
+# Required-Start: network
+# Default-Start: 3 4 5
+# Default-Stop: 0 1 2 6
+# Short-Description: certificate master for Fedora Unified Network Control 'master server only'
+# Description: certificate master to sign/manage ca/cert infrastructure for func
+### END INIT INFO
+
+# chkconfig header
+
+# chkconfig: - 98 99 
+# description:  certificate master to sign/manage ca/cert infrastructure for func
+#
+# processname: /usr/bin/certmaster
+
+# Sanity checks.
+[ -x /usr/bin/certmaster ] || exit 0
+
+SERVICE=certmaster
+PROCESS=certmaster
+DAEMON=/usr/bin/certmaster
+CONFIG_ARGS="--daemon"
+
+CAStatus()
+{
+  ps wt? | grep "$DAEMON" 2>&1 > /dev/null
+  if [ "x$?" = "x0" ]; then
+    RVAL=0
+    echo "certmaster is running"
+  else
+    RVAL=3
+    echo "certmaster is not running"
+  fi
+}
+
+if [ -f /lib/lsb/init-functions ]; then
+  . /lib/lsb/init-functions
+  alias START_DAEMON=start_daemon
+  alias STATUS=CAStatus
+  alias LOG_SUCCESS=log_success_msg
+  alias LOG_FAILURE=log_failure_msg
+  alias LOG_WARNING=log_warning_msg
+elif [ -f /etc/init.d/functions ]; then
+  . /etc/init.d/functions
+  alias START_DAEMON=daemon
+  alias STATUS=status
+  alias LOG_SUCCESS=success
+  alias LOG_FAILURE=failure
+  alias LOG_WARNING=passed
+else
+  echo "Error: your platform is not supported by $0" > /dev/stderr
+  exit 1
+fi
+
+RETVAL=0
+
+start() {
+    echo -n $"Starting certmaster daemon: "
+    START_DAEMON $PROCESS $CONFIG_ARGS
+    RETVAL=$?
+    echo
+    [ $RETVAL -eq 0 ] && touch /var/lock/subsys/$SERVICE
+    return $RETVAL
+}
+
+stop() {
+    echo -n $"Stopping certmaster daemon: "
+    killproc $PROCESS
+    RETVAL=$?
+    echo
+    if [ $RETVAL -eq 0 ]; then
+       rm -f /var/lock/subsys/$SERVICE
+        rm -f /var/run/$SERVICE.pid
+    fi
+}
+
+restart() {
+   stop
+   start
+}
+
+# See how we were called.
+case "$1" in
+    start|stop|restart)
+        $1
+        ;;
+    status)
+        STATUS $PROCESS
+        RETVAL=$?
+        ;;
+    condrestart)
+        [ -f /var/lock/subsys/$SERVICE ] && restart || :
+        ;;
+    reload)
+        echo "can't reload configuration, you have to restart it"
+        RETVAL=$?
+        ;;
+    *)
+        echo $"Usage: $0 {start|stop|status|restart|condrestart|reload}"
+        exit 1
+        ;;
+esac
+exit $RETVAL
+
diff --git a/init-scripts/funcd b/init-scripts/funcd
new file mode 100755 (executable)
index 0000000..63b98a2
--- /dev/null
@@ -0,0 +1,115 @@
+#!/bin/sh
+#
+# funcd    Fedora Unified Network Control
+###################################
+
+# LSB header
+
+### BEGIN INIT INFO
+# Provides: funcd
+# Required-Start: network
+# Required-Stop:
+# Default-Start: 3 4 5
+# Default-Stop: 0 1 2 6
+# Short-Description: Fedora Unified Network Control
+# Description: Crazy simple, secure remote management.
+### END INIT INFO
+
+# chkconfig header
+
+# chkconfig: - 99 99 
+# description:  Crazy simple, secure remote management.
+#
+# processname: /usr/bin/funcd
+
+# Sanity checks.
+[ -x /usr/bin/funcd ] || exit 0
+
+SERVICE=funcd
+PROCESS=funcd
+DAEMON=/usr/bin/funcd
+CONFIG_ARGS="--daemon"
+
+
+FuncStatus()
+{
+  ps wt? | grep "$DAEMON" 2>&1 > /dev/null
+  if [ "x$?" = "x0" ]; then
+    RVAL=0
+    echo "$DAEMON is running"
+  else
+    RVAL=3
+    echo "$DAEMON is not running"
+  fi
+}
+
+if [ -f /lib/lsb/init-functions ]; then
+  . /lib/lsb/init-functions
+  alias START_DAEMON=start_daemon
+  alias STATUS=FuncStatus
+  alias LOG_SUCCESS=log_success_msg
+  alias LOG_FAILURE=log_failure_msg
+  alias LOG_WARNING=log_warning_msg
+elif [ -f /etc/init.d/functions ]; then
+  . /etc/init.d/functions
+  alias START_DAEMON=daemon
+  alias STATUS=status
+  alias LOG_SUCCESS=success
+  alias LOG_FAILURE=failure
+  alias LOG_WARNING=passed
+else
+  echo "Error: your platform is not supported by $0" > /dev/stderr
+  exit 1
+fi
+
+
+RETVAL=0
+
+start() {
+    echo -n $"Starting func daemon: "
+    START_DAEMON $PROCESS $CONFIG_ARGS
+    RETVAL=$?
+    echo
+    [ $RETVAL -eq 0 ] && touch /var/lock/subsys/$SERVICE
+    return $RETVAL
+}
+
+stop() {
+    echo -n $"Stopping func daemon: "
+    killproc $PROCESS
+    RETVAL=$?
+    echo
+    if [ $RETVAL -eq 0 ]; then
+       rm -f /var/lock/subsys/$SERVICE
+        rm -f /var/run/$SERVICE.pid
+    fi
+}
+
+restart() {
+   stop
+   start
+}
+
+# See how we were called.
+case "$1" in
+    start|stop|restart)
+        $1
+        ;;
+    status)
+        STATUS $PROCESS
+        RETVAL=$?
+        ;;
+    condrestart)
+        [ -f /var/lock/subsys/$SERVICE ] && restart || :
+        ;;
+    reload)
+        echo "can't reload configuration, you have to restart it"
+        RETVAL=$?
+        ;;
+    *)
+        echo $"Usage: $0 {start|stop|status|restart|condrestart|reload}"
+        exit 1
+        ;;
+esac
+exit $RETVAL
+
diff --git a/nothing b/nothing
deleted file mode 100644 (file)
index b33c560..0000000
--- a/nothing
+++ /dev/null
@@ -1 +0,0 @@
-test test
diff --git a/scripts/Makefile b/scripts/Makefile
new file mode 100755 (executable)
index 0000000..a4cc7e1
--- /dev/null
@@ -0,0 +1,20 @@
+
+
+PYFILES                = $(wildcard *.py)
+
+PYCHECKER       = /usr/bin/pychecker
+PYFLAKES       = /usr/bin/pyflakes
+
+clean::
+       @rm -fv *.pyc *~ .*~ *.pyo 
+       @find . -name .\#\* -exec rm -fv {} \;
+       @rm -fv *.rpm
+       
+
+pychecker:: 
+       @$(PYCHECKER) $(PYFILES) || exit 0
+
+pyflakes::
+ifneq ($(PYFILES)x, x)
+       @$(PYFLAKES) $(PYFILES) || exit 0
+endif
diff --git a/scripts/certmaster b/scripts/certmaster
new file mode 100755 (executable)
index 0000000..d5f677d
--- /dev/null
@@ -0,0 +1,11 @@
+#!/usr/bin/python
+
+from func import certmaster
+
+import sys
+
+if __name__ == "__main__":
+    certmaster.main(sys.argv)
+    
+
+
diff --git a/scripts/certmaster-ca b/scripts/certmaster-ca
new file mode 100755 (executable)
index 0000000..b3e844a
--- /dev/null
@@ -0,0 +1,92 @@
+#!/usr/bin/python -tt
+# sign/list keys
+# --sign hostname hostname hostname
+# --list # lists all csrs needing to be signed
+# --list-all ?
+# --clean? not sure what it will do
+
+import sys
+import glob
+import os
+
+import func
+import func.certs
+import func.certmaster
+
+
+
+from optparse import OptionParser
+
+def errorprint(stuff):
+    print >> sys.stderr, stuff
+
+
+def parseargs(args):
+    usage = 'certmaster-ca <option> [args]'
+    parser = OptionParser(usage=usage)
+    
+    parser.add_option('-l', '--list', default=False, action="store_true",
+          help='list signing requests remaining')
+    parser.add_option('-s', '--sign', default=False, action="store_true",
+          help='sign requests of hosts specified')
+    parser.add_option('-c', '--clean', default=False, action="store_true",
+          help="clean out all certs or csrs for the hosts specified")
+          
+    (opts, args) = parser.parse_args()
+    
+    
+    if not opts.list and not opts.sign and not opts.clean:
+        parser.print_help()
+        sys.exit(1)
+            
+    return (opts, args)
+
+def main(args):
+    if os.geteuid() != 0:
+        errorprint('Must be root to run certmaster-ca')
+        return 1
+        
+    cm = func.certmaster.CertMaster()
+    
+    (opts, args) = parseargs(args)
+
+        
+    if opts.list:
+        hns = cm.get_csrs_waiting()
+        if hns:
+            for hn in cm.get_csrs_waiting():
+                print hn
+        else:
+           print 'No certificates to sign'
+
+        return 0
+    
+    if opts.sign:
+        if not args:
+            errorprint('Need hostnames to sign')
+            return 1
+            
+        for hn in args:
+            csrglob = '%s/%s.csr' % (cm.cfg.csrroot, hn)
+            csrs = glob.glob(csrglob)
+            if not csrs:
+                errorprint('No match for %s to sign' % hn)
+                return 1
+            
+            for fn in csrs:
+                certfile = cm.sign_this_csr(fn)
+                print '%s signed - cert located at %s' % (fn, certfile)
+        return 0
+    
+    if opts.clean:
+        if not args:
+            errorprint('Need hostname(s) to clean up')
+            return 1
+        
+        for hn in args:
+            cm.remove_this_host(hn)
+        
+        return 0
+
+if __name__ == "__main__":
+    sys.exit(main(sys.argv[1:]))
diff --git a/scripts/func b/scripts/func
new file mode 100755 (executable)
index 0000000..925d6ad
--- /dev/null
@@ -0,0 +1,14 @@
+#!/usr/bin/python
+
+import sys
+import distutils.sysconfig
+
+# sys.path.append("%s/func" % distutils.sysconfig.get_python_lib())
+
+import func.overlord.func_command as func_command
+
+myname, argv = sys.argv[0], sys.argv[1:]
+cli = func_command.FuncCommandLine()
+cli.parse(argv)
+
+
diff --git a/scripts/func-create-module b/scripts/func-create-module
new file mode 100755 (executable)
index 0000000..f2885e8
--- /dev/null
@@ -0,0 +1,79 @@
+#!/usr/bin/env python
+#
+# Copyright 2008, Red Hat, Inc
+# Steve 'Ashcrow' Milner <smilner@redhat.com>
+# John Eckersberg <jeckersb@redhat.com>
+#
+# This software may be freely redistributed under the terms of the GNU
+# general public license.
+#
+# 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., 675 Mass Ave, Cambridge, MA 02139, USA.
+
+TEMPLATE = """\
+#
+# Copyright %s
+# %s <%s>
+#
+# This software may be freely redistributed under the terms of the GNU
+# general public license.
+#
+# 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., 675 Mass Ave, Cambridge, MA 02139, USA.
+
+import func_module
+
+class %s(func_module.FuncModule):
+
+    # Update these if need be.
+    version = "0.0.1"
+    api_version = "0.0.1"
+    description = "%s"
+
+%s
+"""
+
+METHOD_TEMPLATE = '''\
+    def %s(self):
+        """
+        TODO: Document me ...
+        """
+        pass
+
+'''
+
+
+def populate_template(author_name, author_email, module_name, desc, methods):
+    """
+    Makes the method strings and populates the template.
+    """
+    from datetime import datetime
+    
+    actual_methods = ""
+    for method in methods:
+        actual_methods += METHOD_TEMPLATE % method
+    return TEMPLATE % (datetime.now().strftime("%Y"), author_name, 
+                       author_email, module_name, desc, actual_methods[:-2])
+
+
+if __name__ == '__main__':
+    module_name = raw_input("Module Name: ").capitalize()
+    desc = raw_input("Description: ")
+    author_name = raw_input("Author: ")
+    author_email = raw_input("Email: ")
+    methods = []
+    print "\nLeave blank to finish."
+    while True:
+        method = raw_input("Method: ")
+        if method == '':
+            break
+        methods.append(method)
+    # Write it out to a file
+    file_name = "%s.py" % module_name.lower()
+    file_obj = open(file_name, "w")
+    file_obj.write(populate_template(author_name, author_email, 
+                                     module_name, desc, methods))
+    file_obj.close()
+    print "Your module is ready to be hacked on. Wrote out to %s." % file_name
diff --git a/scripts/func-inventory b/scripts/func-inventory
new file mode 100755 (executable)
index 0000000..1a0c36b
--- /dev/null
@@ -0,0 +1,8 @@
+#!/usr/bin/python
+
+import sys
+import distutils.sysconfig
+import func.overlord.inventory as func_inventory
+
+inventory = func_inventory.FuncInventory()
+inventory.run(sys.argv)
diff --git a/scripts/funcd b/scripts/funcd
new file mode 100755 (executable)
index 0000000..3d807bd
--- /dev/null
@@ -0,0 +1,10 @@
+#!/usr/bin/python
+
+
+import sys
+import distutils.sysconfig
+
+from func.minion import server
+
+if __name__ == "__main__":
+    server.main(sys.argv)
diff --git a/setup.py b/setup.py
new file mode 100644 (file)
index 0000000..332ff96
--- /dev/null
+++ b/setup.py
@@ -0,0 +1,73 @@
+#!/usr/bin/python
+
+from distutils.core import setup
+#from setuptools import setup,find_packages
+
+NAME = "func"
+VERSION = open("version", "r+").read().split()[0]
+SHORT_DESC = "%s remote configuration and management api" % NAME
+LONG_DESC = """
+A small pluggable xml-rpc daemon used by %s to implement various web services hooks
+""" % NAME
+
+
+if __name__ == "__main__":
+        manpath    = "share/man/man1/"
+        etcpath    = "/etc/%s" % NAME
+        etcmodpath = "/etc/%s/modules" % NAME
+        initpath   = "/etc/init.d/"
+        logpath    = "/var/log/%s/" % NAME
+       certdir    = "/var/lib/%s/certmaster" % NAME
+        pkipath    = "/etc/pki/%s" % NAME
+        rotpath    = "/etc/logrotate.d"
+        aclpath    = "%s/minion-acl.d" % etcpath
+        setup(
+                name="%s" % NAME,
+                version = VERSION,
+                author = "Lots",
+                author_email = "func-list@redhat.com",
+                url = "https://hosted.fedoraproject.org/projects/func/",
+                license = "GPL",
+               scripts = [
+                     "scripts/funcd", "scripts/func", 
+                     "scripts/certmaster", "scripts/certmaster-ca",
+                     "scripts/func-inventory",
+                     "scripts/func-create-module",
+                ],
+               # package_data = { '' : ['*.*'] },
+                package_dir = {"%s" % NAME: "%s" % NAME
+                },
+               packages = ["%s" % NAME,
+                           "%s/minion" % NAME,
+                           "%s/overlord" % NAME,
+                           "%s/overlord/cmd_modules" % NAME,
+                            "%s/overlord/modules" % NAME,
+                            "%s/minion/modules" % NAME,
+                            # FIXME if there's a clean/easy way to recursively
+                            # find modules then by all means do it, for now
+                            # this will work.
+                            "%s/minion/modules.netapp" % NAME,
+                            "%s/minion/modules.netapp.vol" % NAME
+                ],
+                data_files = [(initpath, ["init-scripts/funcd"]),
+                              (initpath, ["init-scripts/certmaster"]),
+                              (etcpath,  ["etc/minion.conf"]),
+                              (etcpath,  ["etc/certmaster.conf"]),
+                              (etcmodpath, []),
+                              (manpath,  ["docs/func.1.gz"]),
+                              (manpath,  ["docs/func-inventory.1.gz"]),
+                              (manpath,  ["docs/funcd.1.gz"]),
+                              (manpath,  ["docs/certmaster.1.gz"]),
+                              (manpath,  ["docs/certmaster-ca.1.gz"]),
+                             (rotpath,  ['etc/func_rotate']),
+                              (logpath,  []),
+                             (certdir,  []),
+                             (etcpath,  []),
+                             (pkipath,  []),
+                             (aclpath,  [])
+                ],
+                description = SHORT_DESC,
+                long_description = LONG_DESC
+        )
+
diff --git a/version b/version
new file mode 100644 (file)
index 0000000..06da242
--- /dev/null
+++ b/version
@@ -0,0 +1 @@
+0.16 1