summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--planetlab/host.txt396
-rw-r--r--planetlab/pssh/AUTHORS2
-rw-r--r--planetlab/pssh/COPYING32
-rw-r--r--planetlab/pssh/ChangeLog190
-rw-r--r--planetlab/pssh/INSTALL25
-rw-r--r--planetlab/pssh/PKG-INFO28
-rwxr-xr-xplanetlab/pssh/bin/pnuke93
-rwxr-xr-xplanetlab/pssh/bin/prsync125
-rwxr-xr-xplanetlab/pssh/bin/pscp108
-rwxr-xr-xplanetlab/pssh/bin/pslurp129
-rwxr-xr-xplanetlab/pssh/bin/pssh115
-rwxr-xr-xplanetlab/pssh/bin/pssh-askpass11
-rw-r--r--planetlab/pssh/man/man1/pssh.1330
-rw-r--r--planetlab/pssh/psshlib/__init__.py0
-rw-r--r--planetlab/pssh/psshlib/askpass_client.py95
-rw-r--r--planetlab/pssh/psshlib/askpass_server.py101
-rw-r--r--planetlab/pssh/psshlib/cli.py108
-rw-r--r--planetlab/pssh/psshlib/color.py39
-rw-r--r--planetlab/pssh/psshlib/manager.py345
-rw-r--r--planetlab/pssh/psshlib/psshutil.py108
-rw-r--r--planetlab/pssh/psshlib/task.py281
-rw-r--r--planetlab/pssh/setup.py41
-rw-r--r--planetlab/pssh/test/test.py302
23 files changed, 3004 insertions, 0 deletions
diff --git a/planetlab/host.txt b/planetlab/host.txt
new file mode 100644
index 0000000..20a7e9c
--- /dev/null
+++ b/planetlab/host.txt
@@ -0,0 +1,396 @@
+planetlab-01.naist.jp
+charon.cs.binghamton.edu
+planetlab01.erin.utoronto.ca
+planetlab2.csee.usf.edu
+planetlab3.cs.uiuc.edu
+planetlab1.itwm.fhg.de
+planetlab5.cs.duke.edu
+planetlab5.cs.cornell.edu
+planetlab6.cs.cornell.edu
+saturn.cs.brown.edu
+planetlab2.iii.u-tokyo.ac.jp
+planetlab2.cs.ubc.ca
+planlab1.cs.caltech.edu
+planlab2.cs.caltech.edu
+planetlab2.uc.edu
+planetlab3.csail.mit.edu
+planetlab1.cs.cornell.edu
+planetlab6.csail.mit.edu
+planetlab2.nrl.dcs.qmul.ac.uk
+planetlab2.cs.cornell.edu
+planetlab1.n.info.eng.osaka-cu.ac.jp
+planetlab3.n.info.eng.osaka-cu.ac.jp
+pn1-planetlab.huawei.com
+pn2-planetlab.huawei.com
+planetlab3.itwm.fraunhofer.de
+planetlab4.itwm.fraunhofer.de
+planetlab5.itwm.fraunhofer.de
+planetlab1.cs.dartmouth.edu
+planetlab1.simula.no
+mnc2.pusan.ac.kr
+planetlab2.nileu.edu.eg
+planetlab1.cse.msu.edu
+planetlabnode-1.docomolabs-usa.com
+plgmu3.ite.gmu.edu
+planetlab-02.ece.uprm.edu
+mnc1.pusan.ac.kr
+planetlab2.millennium.berkeley.edu
+pli1-pa-4.hpl.hp.com
+planetlab1.tau.ac.il
+planetlab1.millennium.berkeley.edu
+plab1.tidprojects.com
+planet2.prakinf.tu-ilmenau.de
+merkur.planetlab.haw-hamburg.de
+planet1.prakinf.tu-ilmenau.de
+orval.infonet.fundp.ac.be
+pl2-alblf.homeip.net
+planetlab2.di.unito.it
+planetlab1.fem.tu-ilmenau.de
+planetlab3.hiit.fi
+planetlab2.ifi.uio.no
+host147-82-static.93-94-b.business.telecomitalia.it
+planetlab1.informatik.uni-goettingen.de
+planetlab3.di.unito.it
+planetlab2.upc.es
+planet2.inf.tu-dresden.de
+planetlab1.sics.se
+planet2.zib.de
+planetlab1.upc.es
+gschembra3.diit.unict.it
+planetlab2.sics.se
+planetlab1.informatik.uni-erlangen.de
+evghu9.colbud.hu
+evghu4.colbud.hu
+evghu8.colbud.hu
+planetlab-1.di.fc.ul.pt
+planetlab1.ci.pwr.wroc.pl
+planetlab3.xeno.cl.cam.ac.uk
+planetlab1.di.unito.it
+planetlab1.thlab.net
+uoepl1.essex.ac.uk
+onelab3.info.ucl.ac.be
+evghu6.colbud.hu
+planetlab4.williams.edu
+evghu12.colbud.hu
+planetlab3.wail.wisc.edu
+planetlab7.flux.utah.edu
+miranda.planetlab.cs.umd.edu
+planetlab1.utt.fr
+onelab2.info.ucl.ac.be
+planetlab-um00.di.uminho.pt
+zoi.di.uoa.gr
+host3-plb.loria.fr
+planetlab-2.man.poznan.pl
+planetlab2.cs.uoi.gr
+thalescom-48-42.cnt.nerim.net
+planetlab2.ics.forth.gr
+planetlab2.di.fct.unl.pt
+planetlab1.cs.aueb.gr
+planetlab1.science.unitn.it
+planetlab1.fri.uni-lj.si
+planetlab-1.ing.unimo.it
+onelab-1.fhi-fokus.de
+planetlab2.unineuchatel.ch
+planetlab2.cs.uit.no
+planetlab2.polito.it
+planetlab02.tkn.tu-berlin.de
+planetlab4.hiit.fi
+planetlab-2.imperial.ac.uk
+planetlab-2.iscte.pt
+planetlab-1.imag.fr
+planetlab1.uc3m.es
+planetlab-2.tagus.ist.utl.pt
+planetlab1.fct.ualg.pt
+planetlab2.utt.fr
+planetlab1.s3.kth.se
+planet-lab-node2.netgroup.uniroma2.it
+host4-plb.loria.fr
+planetlab1.lkn.ei.tum.de
+planetlab2.tau.ac.il
+plab1-c703.uibk.ac.at
+planetlab2.pjwstk.edu.pl
+planetlab01.dis.unina.it
+onelab6.iet.unipi.it
+planetlab1.cs.vu.nl
+onelab1.info.ucl.ac.be
+dannan.disy.inf.uni-konstanz.de
+dschinni.planetlab.extranet.uni-passau.de
+aladdin.planetlab.extranet.uni-passau.de
+planet2.colbud.hu
+evghu5.colbud.hu
+ple01.fc.univie.ac.at
+planetlab-1.iscte.pt
+planetlab2.cs.vu.nl
+planetlab2.ci.pwr.wroc.pl
+planetlab2.informatik.uni-goettingen.de
+ple2.dmcs.p.lodz.pl
+planetlab2.thlab.net
+iraplab1.iralab.uni-karlsruhe.de
+node2pl.planet-lab.telecom-lille1.eu
+plab2-c703.uibk.ac.at
+planetlab-node1.it-sudparis.eu
+evghu7.colbud.hu
+planetlab1.ifi.uio.no
+planetlab-2.ing.unimo.it
+planetlab2.science.unitn.it
+plab2.create-net.org
+planet1.inf.tu-dresden.de
+planetlab01.ethz.ch
+iraplab2.iralab.uni-karlsruhe.de
+planetlab2.willab.fi
+planetlab-1.research.netlab.hut.fi
+planetlab1.eurecom.fr
+host3.planetlab.informatik.tu-darmstadt.de
+planetlab3.upc.es
+thalescom-48-41.cnt.nerim.net
+planetvs1.informatik.uni-stuttgart.de
+planetlabpc2.upf.edu
+vicky.planetlab.ntua.gr
+dplanet2.uoc.edu
+peeramide.irisa.fr
+planet1.colbud.hu
+planetlab-europe-07.ipv6.lip6.fr
+planetlab1.csg.uzh.ch
+planetlab2.esprit-tn.com
+ple1.tu.koszalin.pl
+planetlab2.fri.uni-lj.si
+ple2.cesnet.cz
+kostis.di.uoa.gr
+ple1.cesnet.cz
+planetlab1.montefiore.ulg.ac.be
+node1pl.planet-lab.telecom-lille1.eu
+planetlab2.uc3m.es
+onelab10.pl.sophia.inria.fr
+onelab11.pl.sophia.inria.fr
+planetlab-2.di.fc.ul.pt
+planetlabpc1.upf.edu
+planetlab2.csg.uzh.ch
+onelab3.warsaw.rd.tp.pl
+planetlab1.cs.uit.no
+ple2.ait.ac.th
+planetlab2.ionio.gr
+planetlab-3.imperial.ac.uk
+marie.iet.unipi.it
+planetlab-europe-02.ipv6.lip6.fr
+planetlab2.informatik.uni-leipzig.de
+rochefort.infonet.fundp.ac.be
+plane-lab-pb1.uni-paderborn.de
+planetlab2.eurecom.fr
+planet2.unipr.it
+planetlab1.esprit-tn.com
+planetlab1.ionio.gr
+planetlab2.exp-math.uni-essen.de
+onelab-2.fhi-fokus.de
+planetlab1lannion.elibel.tm.fr
+planet1.elte.hu
+host2.planetlab.informatik.tu-darmstadt.de
+planetlab1.xeno.cl.cam.ac.uk
+planetlab-wifi-01.ipv6.lip6.fr
+planetlab2.dit.upm.es
+plab2.ple.silweb.pl
+planetlab2.montefiore.ulg.ac.be
+planetlab1.dit.upm.es
+planet1.unipr.it
+peeramidion.irisa.fr
+planetlab1.unineuchatel.ch
+planetlab-2.research.netlab.hut.fi
+evghu14.colbud.hu
+planetlab2.s3.kth.se
+ple1.ait.ac.th
+evghu13.colbud.hu
+planetlab-node3.it-sudparis.eu
+evghu1.colbud.hu
+evghu11.colbud.hu
+planetvs2.informatik.uni-stuttgart.de
+inriarennes1.irisa.fr
+planetlab1.exp-math.uni-essen.de
+planetlab1.urv.cat
+lsirextpc02.epfl.ch
+planetlab4-dsl.cs.cornell.edu
+planet1.scs.stanford.edu
+planetlab1.cs.umass.edu
+planetlab2.cs.umass.edu
+planetlab3.cs.uchicago.edu
+planetlab1.postel.org
+planetlab02.cs.washington.edu
+pl1.csl.utoronto.ca
+planet2.att.nodes.planet-lab.org
+planet1.att.nodes.planet-lab.org
+planet2.scs.stanford.edu
+pl1.6test.edu.cn
+plnode01.cs.mu.oz.au
+plnode02.cs.mu.oz.au
+planetlab2.netlab.uky.edu
+lefthand.eecs.harvard.edu
+righthand.eecs.harvard.edu
+planetlab1.byu.edu
+planetlab2.cs.columbia.edu
+planetlab3.cs.columbia.edu
+plab2.cs.ust.hk
+planetlab03.cs.washington.edu
+server2.planetlab.iit-tech.net
+planetlab1.cesnet.cz
+planetlab2.cesnet.cz
+planetlab3.netmedia.gist.ac.kr
+planetlab1.ucsd.edu
+planetlab2.ucsd.edu
+planet4.cc.gt.atl.ga.us
+planetlab0.dojima.wide.ad.jp
+planetlab1.dojima.wide.ad.jp
+planetlab1.singaren.net.sg
+planetlab2.singaren.net.sg
+planetlab1.cs.ucla.edu
+planetlab1.netlab.uky.edu
+planetlab1-buenosaires.lan.redclara.net
+147-179.surfsnel.dsl.internl.net
+planetlab06.mpi-sws.mpg.de
+pli1-pa-5.hpl.hp.com
+cs-planetlab3.cs.surrey.sfu.ca
+planetlab3.csee.usf.edu
+pl1.ucs.indiana.edu
+pl2.ucs.indiana.edu
+planetlab4.csres.utexas.edu
+cs-planetlab4.cs.surrey.sfu.ca
+planetlab5.csres.utexas.edu
+planetlab4.csee.usf.edu
+netapp6.cs.kookmin.ac.kr
+netapp7.cs.kookmin.ac.kr
+planetlab2.pc.cis.udel.edu
+node1.planetlab.mathcs.emory.edu
+planetlab5.williams.edu
+planetslug4.cse.ucsc.edu
+planetslug5.cse.ucsc.edu
+plab1.larc.usp.br
+plab2.larc.usp.br
+planetlab6.flux.utah.edu
+planetlab1.koganei.wide.ad.jp
+planetlab2.koganei.wide.ad.jp
+planetlab2.ceid.upatras.gr
+planetlab2.cs.ucla.edu
+planetlab4.wail.wisc.edu
+planetlab1.unl.edu
+planetlab3.comp.nus.edu.sg
+planetlab1.utdallas.edu
+planetlab2.utdallas.edu
+server3.planetlab.iit-tech.net
+nis-planet1.doshisha.ac.jp
+nis-planet2.doshisha.ac.jp
+node2.planetlab.mathcs.emory.edu
+planetlab9.millennium.berkeley.edu
+node-2.mcgillplanetlab.org
+planetlab1.bgu.ac.il
+mtuplanetlab2.cs.mtu.edu
+planetlab6.cs.duke.edu
+planetlab-1.cs.auckland.ac.nz
+planetlab-2.cs.auckland.ac.nz
+planetlab02.mpi-sws.mpg.de
+pl1.rcc.uottawa.ca
+pl2.rcc.uottawa.ca
+planetlab2.tamu.edu
+planetlab4.n.info.eng.osaka-cu.ac.jp
+planetlab4.singaren.net.sg
+planetlab1.scsr.nevada.edu
+planetlab2.scsr.nevada.edu
+planetlab-1.calpoly-netlab.net
+planetlab6.goto.info.waseda.ac.jp
+planetlab4.cs.uchicago.edu
+planetlab2.warsaw.rd.tp.pl
+planetlab2.csuohio.edu
+planetlab1.csuohio.edu
+planetlab-1.cs.uic.edu
+planetlab-2.cs.uic.edu
+plnodea.plaust.edu.cn
+flow.colgate.edu
+planetlab-1.webedu.ccu.edu.tw
+planetlab-2.webedu.ccu.edu.tw
+planetlab05.mpi-sws.mpg.de
+planetlab-01.vt.nodes.planet-lab.org
+planetlab7.millennium.berkeley.edu
+planetlab10.millennium.berkeley.edu
+planetlab11.millennium.berkeley.edu
+planetlab1.just.edu.jo
+planetlab8.millennium.berkeley.edu
+planetlab2.bgu.ac.il
+pli1-br-1.hpl.hp.com
+planetlab2.plab.ege.edu.tr
+planetlab1.ntu.nodes.planet-lab.org
+planetlab2.ntu.nodes.planet-lab.org
+planetlab7.cs.duke.edu
+planet11.csc.ncsu.edu
+planetlab5.eecs.umich.edu
+planet12.csc.ncsu.edu
+ebb.colgate.edu
+planetlab5.cs.uiuc.edu
+planetlab6.cs.uiuc.edu
+pl1.pku.edu.cn
+planetlab06.cs.washington.edu
+planetlab1.plab.ege.edu.tr
+pl2snu.koren21.net
+planetlab-03.vt.nodes.planet-lab.org
+planetlab-04.vt.nodes.planet-lab.org
+vn4.cse.wustl.edu
+planetlab05.cs.washington.edu
+planetslug7.cse.ucsc.edu
+pl2.eng.monash.edu.au
+pl2.sos.info.hiroshima-cu.ac.jp
+ricepl-4.cs.rice.edu
+ricepl-5.cs.rice.edu
+pnode2.pdcc-ntu.singaren.net.sg
+pl2.pku.edu.cn
+server4.planetlab.iit-tech.net
+pnode1.pdcc-ntu.singaren.net.sg
+nodea.howard.edu
+nodeb.howard.edu
+planetlab02.just.edu.jo
+plgmu5.ite.gmu.edu
+planetlab1.ecs.vuw.ac.nz
+planetlab2.ecs.vuw.ac.nz
+planet5.cs.ucsb.edu
+planet6.cs.ucsb.edu
+plnode-03.gpolab.bbn.com
+plnode-04.gpolab.bbn.com
+planetlab2.pop-pa.rnp.br
+planetlab1.pop-pa.rnp.br
+planetlab2.cs.uml.edu
+planetlab1.cs.uml.edu
+planetlab-n2.wand.net.nz
+nw142.csie.ncu.edu.tw
+planet1.cs.rit.edu
+planetlab2.clemson.edu
+planetlab1.pjwstk.edu.pl
+planetlab1.clemson.edu
+planetlab-2.elisa.cpsc.ucalgary.ca
+pl2.6test.edu.cn
+planetlab3.rutgers.edu
+planetlab4.rutgers.edu
+planetlab-6.ece.iastate.edu
+planetslug3.cse.ucsc.edu
+planetlab4.netmedia.gist.ac.kr
+planetlab1.ustc.edu.cn
+planetlab1.bupt.edu.cn
+planetlab2.bupt.edu.cn
+planet02.csc.ncsu.edu
+planet1.pnl.nitech.ac.jp
+planet2.pnl.nitech.ac.jp
+planetlab2.tsuniv.edu
+jupiter.planetlab.carleton.ca
+planetlab6.csres.utexas.edu
+planetlab2.pop-rj.rnp.br
+node2.lbnl.nodes.planet-lab.org
+planetlab1.mnlab.cti.depaul.edu
+planetlab2.een.orst.edu
+planetlab1.cs.colorado.edu
+planetlab1.georgetown.edu
+planetlab3.arizona-gigapop.net
+planetlab-1.amst.nodes.planet-lab.org
+planetlab-2.ece.iastate.edu
+planetlab1.engr.uconn.edu
+planetlab2.mnlab.cti.depaul.edu
+planetlab1.ewi.tudelft.nl
+planetlab2.ewi.tudelft.nl
+planetlab1.rutgers.edu
+planetlab3-dsl.cs.cornell.edu
+planetlab-2.fokus.fraunhofer.de
+planetlab1.di.fct.unl.pt
+planet-lab-node1.netgroup.uniroma2.it
+planetlab2.fct.ualg.pt
diff --git a/planetlab/pssh/AUTHORS b/planetlab/pssh/AUTHORS
new file mode 100644
index 0000000..3d0a275
--- /dev/null
+++ b/planetlab/pssh/AUTHORS
@@ -0,0 +1,2 @@
+Andrew McNabb <amcnabb at mcnabbs.org>
+Brent Chun <bnc at theether.org>
diff --git a/planetlab/pssh/COPYING b/planetlab/pssh/COPYING
new file mode 100644
index 0000000..dd89bbd
--- /dev/null
+++ b/planetlab/pssh/COPYING
@@ -0,0 +1,32 @@
+Copyright (c) 2009, Andrew McNabb
+Copyright (c) 2003-2008, Brent N. Chun
+
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+ * Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+
+ * Redistributions in binary form must reproduce the above
+ copyright notice, this list of conditions and the following
+ disclaimer in the documentation and/or other materials provided
+ with the distribution.
+
+ * The names of its contributors may not be used to endorse or
+ promote products derived from this software without specific
+ prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/planetlab/pssh/ChangeLog b/planetlab/pssh/ChangeLog
new file mode 100644
index 0000000..c450ea9
--- /dev/null
+++ b/planetlab/pssh/ChangeLog
@@ -0,0 +1,190 @@
+2011-02-02 Andrew McNabb <amcnabb at mcnabbs.org>
+ * Version 2.2.2
+ - Fixed two crashes (issues 35 and 36). One affects Python <= 2.5
+ and the other affects all of the scripts except pssh.
+2011-01-26 Andrew McNabb <amcnabb at mcnabbs.org>
+ * Version 2.2.1
+ - Fixed a crash when the -l option was used in conjunction with a
+ hosts file (issue #34).
+2011-01-21 Andrew McNabb <amcnabb at mcnabbs.org>
+ * Version 2.2
+ - Added a basic man page for pssh (issue #10).
+ - Fixed askpass to work correctly in the presence of non-password
+ prompts from ssh (issue #23).
+ - Updated the -O option so that it can be specified multiple times
+ (issue #25). Thanks to soham.mehta for the patch.
+ - Fixed host file loader to give an error instead of a backtrace
+ if a file is not found.
+ - Fixed prsync's "-ssh-args" mangling of its argument (issue #24).
+ Thanks to jbyers for the patch.
+ - Fixed some variable names to appease pylint. Thanks to solj for
+ the patch.
+ - Improved pscp to be able to copy multiple local files. Thanks
+ to Carlo Marcelo Arenas Belon for the patch.
+ - Deprecated the PSSH_HOSTS environment variable that seems to
+ cause more problems than it's worth.
+ - Added the ability to set multiple hosts with a single -H flag
+ (issue #26). Thanks to ilya@sukhanov.net for the patch.
+ - Stopped passing "-q" to ssh by default (this masked error messages
+ and reduced usability).
+ - Removed automatic reading from stdin (deprecated in version 2.1).
+ Please use the "-I" option instead.
+ - Added meaningful exit status codes (issue #30).
+ - Other minor fixes
+2010-02-26 Andrew McNabb <amcnabb at mcnabbs.org>
+ * Version 2.1.1
+ - Fixed a problem causing PSSH to crash with Python 2.4.
+2010-02-24 Andrew McNabb <amcnabb at mcnabbs.org>
+ * Version 2.1
+ - Added support for Python 3.0 and 3.1. Although PSSH has only
+ been lightly tested with Python 3, anything that doesn't work
+ in Python 3 is officially a bug.
+ - Added a "-H" option for specifying hosts one-by-one instead of
+ or in addition to a hosts file.
+ - Added "-x" and "-X" options for passing extra command-line
+ arguments to ssh and rsync. Also added a "-S" option to prsync
+ for the special case of passing extra arguments to ssh
+ (issue #2).
+ - Added a "-I" option for specifying that pssh should read from
+ standard input, and added a deprecation warning when standard
+ input is used without this option (issue #12).
+ - Made the command argument optional when the "-I" option is
+ given, so a script can be passed to pssh on standard input
+ (issue #5).
+ - If a username or port is given, these are now included in the
+ output filename, which allows different connections to be
+ distinguished from each other (issue #7).
+ - Added the pssh-askpass wrapper as a standalone script because
+ setup.py was removing the executable bit from askpass.py. This
+ fixes a "permission denied" error when using the -A option
+ (issue #8).
+ - Fixed a problem where pssh was unnecessarily specifying a
+ username (issue #14).
+ - Fixed a delay due to a lost SIGCHLD signal.
+ - Removed extra spaces between output chunks in outdir files
+ (issue #6). Thanks to knutsen for the fix.
+ - Fixed a bug where pscp passed the wrong option for sending scp
+ a custom port. Thanks to Jan Rafaj for the patch.
+ - Fixed prsync to send the port as an option to ssh (issue #1).
+ Thanks to Ryan Brothers for the fix.
+2009-10-20 Andrew McNabb <amcnabb at mcnabbs.org>
+ * Version 2.0
+ - Rewrote communication code to be more efficient. PSSH now
+ operates with only one or two threads.
+ - Added the ability to interrupt PSSH (with CTRL-c).
+ - Added an option to prompt for a password.
+ - Refactored code into a distinct library (psshlib).
+2008-10-12 Brent N. Chun <bnc at theether.org>
+ * Version 1.4.3
+ - Fixed bug in select_wrap. If timeout is None (e.g., the
+ default for prsync, etc.), then never time out. Bug reported
+ by Carlo Marcelo Arenas Belon (carenas at sajinet.com.pe).
+ - Catch getopt exceptions and print usage as well as getopt
+ exception string. Contribution from Carlo Marcelo Arenas
+ Belon (carenas at sajinet.com.pe).
+ - Added contribution from Bas van der Vlies (basv at sara.nl)
+ to allow comments in hosts file. Comments must begin with
+ # character (leading whitespace is also allowed).
+ - Restore file status bugs after reading stdin in pssh.
+ - Fixed typo bug in pslurp when using options and in
+ non-recursive mode.
+ - Removed conflicts with built-in names.
+2008-09-01 Brent N. Chun <bnc at theether.org>
+ * Version 1.4.2
+ - Fixed minor bug: select returns select.error on an error,
+ not OSError.
+2008-08-27 Brent N. Chun <bnc at theether.org>
+ * Version 1.4.1
+ - Removed broken SIGCHLD handler.
+ - Refining subprocess _cleanup dynamically to an empty lambda
+ function since subprocess is not thread-safe and we already
+ call wait on child processes ourselves anyway.
+ - Adding missing verbose flag to rest of bin/* programs.
+2008-08-24 Brent N. Chun <bnc at theether.org>
+ * Version 1.4.0
+ - Fixed 64-bit bug in pslurp, pscp, prsync. Previously, the
+ default select timeout was sys.maxint, but this is a 64-bit
+ value on 64-machines. Now using None when calling select when
+ there is no timeout. Bug reported by (buixor at gmail.com).
+ - Catching EINTR and ignoring it for select, read, write in
+ BaseThread class.
+ - Fixed longopts for pnuke, prsync, pscp, pslurp, pssh (bug
+ reported a Debian user via Andrew Pollock (apollock at
+ debian.org)).
+ Reference: "bug #481901: pssh: options mis-specified"
+ - Added missing environment variables for options for pnuke,
+ prsync, pscp, pslurp, pssh.
+2008-06-04 Brent N. Chun <bnc at theether.org>
+ * Version 1.3.2
+ - Added shortopts bug fix from Lev Givon (lev at
+ columbia.edu) in bin/pssh.
+2007-04-11 Brent N. Chun <bnc at theether.org>
+ * Version 1.3.1
+ - Reverted I/O back to 1.2.2. style pssh I/O.
+2007-04-10 Brent N. Chun <bnc at theether.org>
+ * Version 1.3.0
+ - Added contributions from Deepak Giridharagopal (deepak at
+ brownman.org)
+ * Added ANSI color to pssh, pscp, etc. output.
+ * Each status message now includes a timestamp.
+ * Failures now indicate the cause (e.g., timeout, etc.)
+ * Intermediate directories are created as needed for output.
+ * Removed use of setsid in shell exec.
+ * Using Exception objects rather than raw strings.
+ * Added support for piping stdin to each ssh process.
+ * Added -i option to pssh for "inlining" output to stdout.
+ * Added Python setup.py file for a standard install.
+ - Switched to BSD license.
+2006-06-18 Brent N. Chun <bnc at theether.org>
+ * Version 1.2.2
+ - Added patch from Dan Silverstein (dans at pch.net) to fix
+ parsecmdline bug.
+2005-12-31 Brent N. Chun <bnc at theether.org>
+ * Version 1.2.1
+ - Changed sys.path so pssh can run without RPM
+ install.
+ - Changed RPM library files to install in
+ /usr/local/lib/python
+ - make install and make uninstall now work as
+ expected for installations from source.
+2004-11-10 Brent N. Chun <bnc at intel-research.net>
+ * Added patch from Dave Barr <barr at google.com>
+ - Adds -a, -z flags to prsync
+2004-10-05 Brent N. Chun <bnc at intel-research.net>
+ * Default user is now current user in all programs (on
+ suggestion from Jim Wight <j.k.wight at newcastle.ac.uk>).
+ * Fixed path typo on prsync from 1.1.0 release
+ * Version 1.1.1
+2004-10-04 Brent N. Chun <bnc at intel-research.net>
+ * Added patch from Dave Barr <barr at google.com>
+ - Adds an ssh options flag (-O) to prsync
+ * Added patch from Chad Yoshikawa <chadyoshikawa at gmail.com>
+ - Adds a print to stdout flag (-P) to pssh
+ * Version 1.1.0
+2004-08-21 Brent N. Chun <bnc at intel-research.net>
+ * All cmds now take -o, -e for stdout, stderr
+ * Checking return values for all cmds
+ * Factored common thread structure out of all cmds
+ * Changed pslurp's dir for local to -L, rather than -o (stdout)
+ * Version 1.0.0
+2003-11-20 Brent N. Chun <bnc at intel-research.net>
+ * Added handler for SIGCHLD
+ * Wait for all threads before returning to main thread
+ * Kill all straggler processes when done
+2003-11-18 Brent N. Chun <bnc at intel-research.net>
+ * Added pslurp (scp from remote nodes), updated to 0.2.3
+2003-11-12 Brent N. Chun <bnc at intel-research.net>
+ * Fixed read bug, so all output is obtained.
+ * Added timeout option (defaults to None for pscp/prsync)
+ * Added verbose option for pssh/pnuke (this is -q or not)
+ * Added environment variables for options
+ * Fixed usage for pnuke
+2003-09-06 Brent N. Chun <bnc at intel-research.net>
+ * Added -O for pssh, pscp, and pnuke for passing SSH options
+ * Changed order of options in usage (required, optional)
+2003-09-06 Brent N. Chun <bnc at intel-research.net>
+ * Added parallel rsync (prsync)
+ * Added support for "host[:port] user" lines in hosts files
+ * Factored a bit of code out into lib/python/psshutil.py
+2003-08-16 Brent N. Chun <bnc at intel-research.net>
+ * Initial version (0.1.0)
diff --git a/planetlab/pssh/INSTALL b/planetlab/pssh/INSTALL
new file mode 100644
index 0000000..ff6adc6
--- /dev/null
+++ b/planetlab/pssh/INSTALL
@@ -0,0 +1,25 @@
+Installing PSSH
+===============
+
+If you don't already have setuptools installed:
+
+ # wget 'http://peak.telecommunity.com/dist/ez_setup.py'
+ # sudo python ez_setup.py
+
+Then:
+
+ # sudo python setup.py install
+
+Share and enjoy!
+
+
+Instructions for Packagers
+--------------------------
+
+Packagers create RPM or deb files for Linux distributions. If you are a
+normal user, please skip this section.
+
+Packaging PSSH is pretty straightforward. Just do a setup.py install, and
+then move usr/bin/pssh-askpass to usr/libexec/pssh/pssh-askpass. Although
+having pssh-askpass in /usr/bin doesn't really hurt anything, it's not
+necessary, so it's cleaner to move it to /usr/libexec.
diff --git a/planetlab/pssh/PKG-INFO b/planetlab/pssh/PKG-INFO
new file mode 100644
index 0000000..55818a1
--- /dev/null
+++ b/planetlab/pssh/PKG-INFO
@@ -0,0 +1,28 @@
+Metadata-Version: 1.0
+Name: pssh
+Version: 2.2.2
+Summary: Parallel version of OpenSSH and related tools
+Home-page: http://code.google.com/p/parallel-ssh/
+Author: Andrew McNabb
+Author-email: amcnabb@mcnabbs.org
+License: BSD
+Description: PSSH (Parallel SSH) provides parallel versions of OpenSSH and related tools, including pssh, pscp, prsync, pnuke, and pslurp. The project includes psshlib which can be used within custom applications.
+Platform: linux
+Classifier: Development Status :: 5 - Production/Stable
+Classifier: Intended Audience :: System Administrators
+Classifier: License :: OSI Approved :: BSD License
+Classifier: Operating System :: POSIX
+Classifier: Programming Language :: Python
+Classifier: Programming Language :: Python :: 2
+Classifier: Programming Language :: Python :: 2.4
+Classifier: Programming Language :: Python :: 2.5
+Classifier: Programming Language :: Python :: 2.6
+Classifier: Programming Language :: Python :: 2.7
+Classifier: Programming Language :: Python :: 3
+Classifier: Programming Language :: Python :: 3.0
+Classifier: Programming Language :: Python :: 3.1
+Classifier: Programming Language :: Python :: 3.2
+Classifier: Topic :: Software Development :: Libraries :: Python Modules
+Classifier: Topic :: System :: Clustering
+Classifier: Topic :: System :: Networking
+Classifier: Topic :: System :: Systems Administration
diff --git a/planetlab/pssh/bin/pnuke b/planetlab/pssh/bin/pnuke
new file mode 100755
index 0000000..2b4feb5
--- /dev/null
+++ b/planetlab/pssh/bin/pnuke
@@ -0,0 +1,93 @@
+#!/usr/bin/env python
+# -*- Mode: python -*-
+
+# Copyright (c) 2009, Andrew McNabb
+# Copyright (c) 2003-2008, Brent N. Chun
+
+"""Nukes all processes that match pattern running as user on the set of nodes
+in hosts.txt.
+"""
+
+import os
+import sys
+
+parent, bindir = os.path.split(os.path.dirname(os.path.abspath(sys.argv[0])))
+if os.path.exists(os.path.join(parent, 'psshlib')):
+ sys.path.insert(0, parent)
+
+from psshlib import psshutil
+from psshlib.task import Task
+from psshlib.manager import Manager, FatalError
+from psshlib.cli import common_parser, common_defaults
+
+_DEFAULT_TIMEOUT = 60
+
+def option_parser():
+ parser = common_parser()
+ parser.usage = "%prog [OPTIONS] -h hosts.txt pattern"
+ parser.epilog = "Example: pnuke -h hosts.txt -l irb2 java"
+ return parser
+
+def parse_args():
+ parser = option_parser()
+ defaults = common_defaults(timeout=_DEFAULT_TIMEOUT)
+ parser.set_defaults(**defaults)
+ opts, args = parser.parse_args()
+
+ if len(args) < 1:
+ parser.error('Pattern not specified.')
+
+ if len(args) > 1:
+ parser.error('Extra arguments given after the pattern.')
+
+ if not opts.host_files and not opts.host_strings:
+ parser.error('Hosts not specified.')
+
+ return opts, args
+
+def do_pnuke(hosts, pattern, opts):
+ if opts.outdir and not os.path.exists(opts.outdir):
+ os.makedirs(opts.outdir)
+ if opts.errdir and not os.path.exists(opts.errdir):
+ os.makedirs(opts.errdir)
+ manager = Manager(opts)
+ for host, port, user in hosts:
+ cmd = ['ssh', host, '-o', 'NumberOfPasswordPrompts=1']
+ if opts.options:
+ for opt in opts.options:
+ cmd += ['-o', opt]
+ if user:
+ cmd += ['-l', user]
+ if port:
+ cmd += ['-p', port]
+ if opts.extra:
+ cmd.extend(opts.extra)
+ cmd.append('pkill -9 %s' % pattern)
+ t = Task(host, port, user, cmd, opts)
+ manager.add_task(t)
+ try:
+ statuses = manager.run()
+ except FatalError:
+ sys.exit(1)
+
+ if min(statuses) < 0:
+ # At least one process was killed.
+ sys.exit(3)
+ for status in statuses:
+ if status != 0:
+ sys.exit(4)
+
+if __name__ == "__main__":
+ opts, args = parse_args()
+ pattern = args[0]
+ try:
+ hosts = psshutil.read_host_files(opts.host_files,
+ default_user=opts.user)
+ except IOError:
+ _, e, _ = sys.exc_info()
+ sys.stderr.write('Could not open hosts file: %s\n' % e.strerror)
+ sys.exit(1)
+ if opts.host_strings:
+ for s in opts.host_strings:
+ hosts.extend(psshutil.parse_host_string(s, default_user=opts.user))
+ do_pnuke(hosts, pattern, opts)
diff --git a/planetlab/pssh/bin/prsync b/planetlab/pssh/bin/prsync
new file mode 100755
index 0000000..b66443b
--- /dev/null
+++ b/planetlab/pssh/bin/prsync
@@ -0,0 +1,125 @@
+#!/usr/bin/env python
+# -*- Mode: python -*-
+
+# Copyright (c) 2009, Andrew McNabb
+# Copyright (c) 2003-2008, Brent N. Chun
+
+"""Parallel rsync to the set of nodes in hosts.txt.
+
+For each node, we essentially do a rsync -rv -e ssh local user@host:remote.
+Note that remote must be an absolute path.
+"""
+
+import os
+import re
+import sys
+
+parent, bindir = os.path.split(os.path.dirname(os.path.abspath(sys.argv[0])))
+if os.path.exists(os.path.join(parent, 'psshlib')):
+ sys.path.insert(0, parent)
+
+from psshlib import psshutil
+from psshlib.task import Task
+from psshlib.manager import Manager, FatalError
+from psshlib.cli import common_parser, common_defaults
+
+def option_parser():
+ parser = common_parser()
+ parser.usage = "%prog [OPTIONS] -h hosts.txt local remote"
+ parser.epilog = ("Example: prsync -r -h hosts.txt -l irb2 foo " +
+ "/home/irb2/foo")
+
+ parser.add_option('-r', '--recursive', dest='recursive',
+ action='store_true', help='recusively copy directories (OPTIONAL)')
+ parser.add_option('-a', '--archive', dest='archive', action='store_true',
+ help='use rsync -a (archive mode) (OPTIONAL)')
+ parser.add_option('-z', '--compress', dest='compress', action='store_true',
+ help='use rsync compression (OPTIONAL)')
+ parser.add_option('-S', '--ssh-args', metavar="ARGS", dest='ssh_args',
+ action='store', help='extra arguments for ssh')
+
+ return parser
+
+def parse_args():
+ parser = option_parser()
+ defaults = common_defaults()
+ parser.set_defaults(**defaults)
+ opts, args = parser.parse_args()
+
+ if len(args) < 1:
+ parser.error('Paths not specified.')
+
+ if len(args) < 2:
+ parser.error('Remote path not specified.')
+
+ if len(args) > 2:
+ parser.error('Extra arguments given after the remote path.')
+
+ if not opts.host_files and not opts.host_strings:
+ parser.error('Hosts not specified.')
+
+ return opts, args
+
+def do_prsync(hosts, local, remote, opts):
+ if opts.outdir and not os.path.exists(opts.outdir):
+ os.makedirs(opts.outdir)
+ if opts.errdir and not os.path.exists(opts.errdir):
+ os.makedirs(opts.errdir)
+ manager = Manager(opts)
+ for host, port, user in hosts:
+ ssh = ['ssh']
+ if opts.options:
+ ssh += ['-o', opts.options]
+ if port:
+ ssh += ['-p', port]
+ if opts.ssh_args:
+ ssh += [opts.ssh_args]
+
+ cmd = ['rsync', '-e', ' '.join(ssh)]
+ if opts.verbose:
+ cmd.append('-v')
+ if opts.recursive:
+ cmd.append('-r')
+ if opts.archive:
+ cmd.append('-a')
+ if opts.compress:
+ cmd.append('-z')
+ if opts.extra:
+ cmd.extend(opts.extra)
+ cmd.append(local)
+ if user:
+ cmd.append('%s@%s:%s' % (user, host, remote))
+ else:
+ cmd.append('%s:%s' % (host, remote))
+ t = Task(host, port, user, cmd, opts)
+ manager.add_task(t)
+ try:
+ statuses = manager.run()
+ except FatalError:
+ sys.exit(1)
+
+ if min(statuses) < 0:
+ # At least one process was killed.
+ sys.exit(3)
+ for status in statuses:
+ if status != 0:
+ sys.exit(4)
+
+if __name__ == "__main__":
+ opts, args = parse_args()
+ local = args[0]
+ remote = args[1]
+ if not re.match("^/", remote):
+ print("Remote path %s must be an absolute path" % remote)
+ sys.exit(3)
+ try:
+ hosts = psshutil.read_host_files(opts.host_files,
+ default_user=opts.user)
+ except IOError:
+ _, e, _ = sys.exc_info()
+ sys.stderr.write('Could not open hosts file: %s\n' % e.strerror)
+ sys.exit(1)
+ if opts.host_strings:
+ for s in opts.host_strings:
+ hosts.extend(psshutil.parse_host_string(s, default_user=opts.user))
+ do_prsync(hosts, local, remote, opts)
diff --git a/planetlab/pssh/bin/pscp b/planetlab/pssh/bin/pscp
new file mode 100755
index 0000000..3caf455
--- /dev/null
+++ b/planetlab/pssh/bin/pscp
@@ -0,0 +1,108 @@
+#!/usr/bin/env python
+# -*- Mode: python -*-
+
+# Copyright (c) 2009, Andrew McNabb
+# Copyright (c) 2003-2008, Brent N. Chun
+
+"""Parallel scp to the set of nodes in hosts.txt.
+
+For each node, we essentially do a scp [-r] local user@host:remote. This
+program also uses the -q (quiet) and -C (compression) options. Note that
+remote must be an absolute path.
+"""
+
+import os
+import re
+import sys
+
+parent, bindir = os.path.split(os.path.dirname(os.path.abspath(sys.argv[0])))
+if os.path.exists(os.path.join(parent, 'psshlib')):
+ sys.path.insert(0, parent)
+
+from psshlib import psshutil
+from psshlib.task import Task
+from psshlib.manager import Manager, FatalError
+from psshlib.cli import common_parser, common_defaults
+
+def option_parser():
+ parser = common_parser()
+ parser.usage = "%prog [OPTIONS] -h hosts.txt local remote"
+ parser.epilog = ("Example: pscp -h hosts.txt -l irb2 foo.txt " +
+ "/home/irb2/foo.txt")
+
+ parser.add_option('-r', '--recursive', dest='recursive',
+ action='store_true', help='recusively copy directories (OPTIONAL)')
+
+ return parser
+
+def parse_args():
+ parser = option_parser()
+ defaults = common_defaults()
+ parser.set_defaults(**defaults)
+ opts, args = parser.parse_args()
+
+ if len(args) < 1:
+ parser.error('Paths not specified.')
+
+ if len(args) < 2:
+ parser.error('Remote path not specified.')
+
+ if not opts.host_files and not opts.host_strings:
+ parser.error('Hosts not specified.')
+
+ return opts, args
+
+def do_pscp(hosts, localargs, remote, opts):
+ if opts.outdir and not os.path.exists(opts.outdir):
+ os.makedirs(opts.outdir)
+ if opts.errdir and not os.path.exists(opts.errdir):
+ os.makedirs(opts.errdir)
+ manager = Manager(opts)
+ for host, port, user in hosts:
+ cmd = ['scp', '-qC']
+ if opts.options:
+ for opt in opts.options:
+ cmd += ['-o', opt]
+ if port:
+ cmd += ['-P', port]
+ if opts.recursive:
+ cmd.append('-r')
+ if opts.extra:
+ cmd.extend(opts.extra)
+ cmd.extend(localargs)
+ if user:
+ cmd.append('%s@%s:%s' % (user, host, remote))
+ else:
+ cmd.append('%s:%s' % (host, remote))
+ t = Task(host, port, user, cmd, opts)
+ manager.add_task(t)
+ try:
+ statuses = manager.run()
+ except FatalError:
+ sys.exit(1)
+
+ if min(statuses) < 0:
+ # At least one process was killed.
+ sys.exit(3)
+ for status in statuses:
+ if status != 0:
+ sys.exit(4)
+
+if __name__ == "__main__":
+ opts, args = parse_args()
+ localargs = args[0:-1]
+ remote = args[-1]
+ if not re.match("^/", remote):
+ print("Remote path %s must be an absolute path" % remote)
+ sys.exit(3)
+ try:
+ hosts = psshutil.read_host_files(opts.host_files,
+ default_user=opts.user)
+ except IOError:
+ _, e, _ = sys.exc_info()
+ sys.stderr.write('Could not open hosts file: %s\n' % e.strerror)
+ sys.exit(1)
+ if opts.host_strings:
+ for s in opts.host_strings:
+ hosts.extend(psshutil.parse_host_string(s, default_user=opts.user))
+ do_pscp(hosts, localargs, remote, opts)
diff --git a/planetlab/pssh/bin/pslurp b/planetlab/pssh/bin/pslurp
new file mode 100755
index 0000000..5797e7c
--- /dev/null
+++ b/planetlab/pssh/bin/pslurp
@@ -0,0 +1,129 @@
+#!/usr/bin/env python
+# -*- Mode: python -*-
+
+# Copyright (c) 2009, Andrew McNabb
+# Copyright (c) 2003-2008, Brent N. Chun
+
+"""Parallel scp from the set of nodes in hosts.txt.
+
+For each node, we essentially do a scp [-r] user@host:remote
+outdir/<node>/local. This program also uses the -q (quiet) and -C
+(compression) options. Note that remote must be an absolute path.
+"""
+
+import os
+import re
+import sys
+
+parent, bindir = os.path.split(os.path.dirname(os.path.abspath(sys.argv[0])))
+if os.path.exists(os.path.join(parent, 'psshlib')):
+ sys.path.insert(0, parent)
+
+from psshlib import psshutil
+from psshlib.task import Task
+from psshlib.manager import Manager, FatalError
+from psshlib.cli import common_parser, common_defaults
+
+def option_parser():
+ parser = common_parser()
+ parser.usage = "%prog [OPTIONS] -h hosts.txt remote local"
+ parser.epilog = ("Example: pslurp -h hosts.txt -L /tmp/outdir -l irb2 " +
+ " /home/irb2/foo.txt foo.txt")
+
+ parser.add_option('-r', '--recursive', dest='recursive',
+ action='store_true', help='recusively copy directories (OPTIONAL)')
+ parser.add_option('-L', '--localdir', dest='localdir',
+ help='output directory for remote file copies')
+
+ return parser
+
+def parse_args():
+ parser = option_parser()
+ defaults = common_defaults()
+ parser.set_defaults(**defaults)
+ opts, args = parser.parse_args()
+
+ if len(args) < 1:
+ parser.error('Paths not specified.')
+
+ if len(args) < 2:
+ parser.error('Local path not specified.')
+
+ if len(args) > 2:
+ parser.error('Extra arguments given after the local path.')
+
+ if not opts.host_files and not opts.host_strings:
+ parser.error('Hosts not specified.')
+
+ return opts, args
+
+def do_pslurp(hosts, remote, local, opts):
+ if opts.localdir and not os.path.exists(opts.localdir):
+ os.makedirs(opts.localdir)
+ if opts.outdir and not os.path.exists(opts.outdir):
+ os.makedirs(opts.outdir)
+ if opts.errdir and not os.path.exists(opts.errdir):
+ os.makedirs(opts.errdir)
+ for host, port, user in hosts:
+ if opts.localdir:
+ dirname = "%s/%s" % (opts.localdir, host)
+ else:
+ dirname = host
+ if not os.path.exists(dirname):
+ os.mkdir(dirname)
+ manager = Manager(opts)
+ for host, port, user in hosts:
+ if opts.localdir:
+ localpath = "%s/%s/%s" % (opts.localdir, host, local)
+ else:
+ localpath = "%s/%s" % (host, local)
+ cmd = ['scp', '-qC']
+ if opts.options:
+ for opt in opts.options:
+ cmd += ['-o', opt]
+ if port:
+ cmd += ['-P', port]
+ if opts.recursive:
+ cmd.append('-r')
+ if opts.extra:
+ cmd.extend(opts.extra)
+ if user:
+ cmd.append('%s@%s:%s' % (user, host, remote))
+ else:
+ cmd.append('%s:%s' % (host, remote))
+ cmd.append(localpath)
+ t = Task(host, port, user, cmd, opts)
+ manager.add_task(t)
+ try:
+ statuses = manager.run()
+ except FatalError:
+ sys.exit(1)
+
+ if min(statuses) < 0:
+ # At least one process was killed.
+ sys.exit(3)
+ for status in statuses:
+ if status == 255:
+ sys.exit(4)
+ for status in statuses:
+ if status != 0:
+ sys.exit(5)
+
+if __name__ == "__main__":
+ opts, args = parse_args()
+ remote = args[0]
+ local = args[1]
+ if not re.match("^/", remote):
+ print("Remote path %s must be an absolute path" % remote)
+ sys.exit(3)
+ try:
+ hosts = psshutil.read_host_files(opts.host_files,
+ default_user=opts.user)
+ except IOError:
+ _, e, _ = sys.exc_info()
+ sys.stderr.write('Could not open hosts file: %s\n' % e.strerror)
+ sys.exit(1)
+ if opts.host_strings:
+ for s in opts.host_strings:
+ hosts.extend(psshutil.parse_host_string(s, default_user=opts.user))
+ do_pslurp(hosts, remote, local, opts)
diff --git a/planetlab/pssh/bin/pssh b/planetlab/pssh/bin/pssh
new file mode 100755
index 0000000..f9af471
--- /dev/null
+++ b/planetlab/pssh/bin/pssh
@@ -0,0 +1,115 @@
+#!/usr/bin/env python
+# -*- Mode: python -*-
+
+# Copyright (c) 2009, Andrew McNabb
+# Copyright (c) 2003-2008, Brent N. Chun
+
+"""Parallel ssh to the set of nodes in hosts.txt.
+
+For each node, this essentially does an "ssh host -l user prog [arg0] [arg1]
+...". The -o option can be used to store stdout from each remote node in a
+directory. Each output file in that directory will be named by the
+corresponding remote node's hostname or IP address.
+"""
+
+import fcntl
+import os
+import sys
+
+parent, bindir = os.path.split(os.path.dirname(os.path.abspath(sys.argv[0])))
+if os.path.exists(os.path.join(parent, 'psshlib')):
+ sys.path.insert(0, parent)
+
+from psshlib import psshutil
+from psshlib.manager import Manager, FatalError
+from psshlib.task import Task
+from psshlib.cli import common_parser, common_defaults
+
+_DEFAULT_TIMEOUT = 60
+
+def option_parser():
+ parser = common_parser()
+ parser.usage = "%prog [OPTIONS] command [...]"
+ parser.epilog = "Example: pssh -h hosts.txt -l irb2 -o /tmp/foo uptime"
+
+ parser.add_option('-i', '--inline', dest='inline', action='store_true',
+ help='inline aggregated output for each server')
+ parser.add_option('-I', '--send-input', dest='send_input',
+ action='store_true',
+ help='read from standard input and send as input to ssh')
+ parser.add_option('-P', '--print', dest='print_out', action='store_true',
+ help='print output as we get it')
+
+ return parser
+
+def parse_args():
+ parser = option_parser()
+ defaults = common_defaults(timeout=_DEFAULT_TIMEOUT)
+ parser.set_defaults(**defaults)
+ opts, args = parser.parse_args()
+
+ if len(args) == 0 and not opts.send_input:
+ parser.error('Command not specified.')
+
+ if not opts.host_files and not opts.host_strings:
+ parser.error('Hosts not specified.')
+
+ return opts, args
+
+def do_pssh(hosts, cmdline, opts):
+ if opts.outdir and not os.path.exists(opts.outdir):
+ os.makedirs(opts.outdir)
+ if opts.errdir and not os.path.exists(opts.errdir):
+ os.makedirs(opts.errdir)
+ if opts.send_input:
+ stdin = sys.stdin.read()
+ else:
+ stdin = None
+ manager = Manager(opts)
+ for host, port, user in hosts:
+ cmd = ['ssh', host, '-o', 'NumberOfPasswordPrompts=1',
+ '-o', 'SendEnv=PSSH_NODENUM']
+ if opts.options:
+ for opt in opts.options:
+ cmd += ['-o', opt]
+ if user:
+ cmd += ['-l', user]
+ if port:
+ cmd += ['-p', port]
+ if opts.extra:
+ cmd.extend(opts.extra)
+ if cmdline:
+ cmd.append(cmdline)
+ t = Task(host, port, user, cmd, opts, stdin)
+ manager.add_task(t)
+ try:
+ statuses = manager.run()
+ except FatalError:
+ sys.exit(1)
+
+ if min(statuses) < 0:
+ # At least one process was killed.
+ sys.exit(3)
+ # The any builtin was introduced in Python 2.5 (so we can't use it yet):
+ #elif any(x==255 for x in statuses):
+ for status in statuses:
+ if status == 255:
+ sys.exit(4)
+ for status in statuses:
+ if status != 0:
+ sys.exit(5)
+
+if __name__ == "__main__":
+ opts, args = parse_args()
+ cmdline = " ".join(args)
+ try:
+ hosts = psshutil.read_host_files(opts.host_files,
+ default_user=opts.user)
+ except IOError:
+ _, e, _ = sys.exc_info()
+ sys.stderr.write('Could not open hosts file: %s\n' % e.strerror)
+ sys.exit(1)
+ if opts.host_strings:
+ for s in opts.host_strings:
+ hosts.extend(psshutil.parse_host_string(s, default_user=opts.user))
+ do_pssh(hosts, cmdline, opts)
diff --git a/planetlab/pssh/bin/pssh-askpass b/planetlab/pssh/bin/pssh-askpass
new file mode 100755
index 0000000..beb9e8b
--- /dev/null
+++ b/planetlab/pssh/bin/pssh-askpass
@@ -0,0 +1,11 @@
+#!/usr/bin/env python
+
+import os
+import sys
+
+parent, bindir = os.path.split(os.path.dirname(os.path.abspath(sys.argv[0])))
+if os.path.exists(os.path.join(parent, 'psshlib')):
+ sys.path.insert(0, parent)
+
+from psshlib.askpass_client import askpass_main
+askpass_main()
diff --git a/planetlab/pssh/man/man1/pssh.1 b/planetlab/pssh/man/man1/pssh.1
new file mode 100644
index 0000000..27d9e2a
--- /dev/null
+++ b/planetlab/pssh/man/man1/pssh.1
@@ -0,0 +1,330 @@
+.\" Man page for pssh. See "man 7 man" and "man man-pages" for formatting info.
+.TH pssh 1 "February 25, 2010"
+
+.SH NAME
+pssh \(em parallel ssh program
+
+
+.SH SYNOPSIS
+.B pssh
+.RB [ \-vAiIP ]
+.RB [ \-h
+.IR hosts_file ]
+.RB [ \-H
+.RI [ user @] host [: port ]]
+.RB [ \-l
+.IR user ]
+.RB [ \-p
+.IR par ]
+.RB [ \-o
+.IR outdir ]
+.RB [ \-e
+.IR errdir ]
+.RB [ \-t
+.IR timeout ]
+.RB [ \-O
+.IR options ]
+.RB [ \-x
+.IR args ]
+.RB [ \-X
+.IR arg ]
+.I command ...
+
+.B pssh \-I
+.RB [ \-vAiIP ]
+.RB [ \-h
+.IR hosts_file ]
+.RB [ \-H
+.RI [ user @] host [: port ]]
+.RB [ \-l
+.IR user ]
+.RB [ \-p
+.IR par ]
+.RB [ \-o
+.IR outdir ]
+.RB [ \-e
+.IR errdir ]
+.RB [ \-t
+.IR timeout ]
+.RB [ \-O
+.IR options ]
+.RB [ \-x
+.IR args ]
+.RB [ \-X
+.IR arg ]
+.RI [ command
+.IR ... ]
+
+
+.SH DESCRIPTION
+.PP
+.B pssh
+is a program for executing ssh in parallel on a number of hosts. It provides
+features such as sending input to all of the processes, passing a password
+to ssh, saving output to files, and timing out.
+
+
+.SH OPTIONS
+
+.TP
+.BI \-h " host_file"
+.PD 0
+.TP
+.BI \-\-hosts " host_file"
+Read hosts from the given
+.IR host_file .
+Lines in the host file are of the form
+.RI [ user @] host [: port ]
+and can include blank lines and comments (lines beginning with "#").
+If multiple host files are given (the
+.B \-h
+option is used more than once), then pssh behaves as though these files
+were concatenated together.
+If a host is specified specified multiple times, then pssh will connect the
+given number of times.
+
+.TP
+.B \-H
+.RI [ user @] host [: port ]
+.PD 0
+.TP
+.B \-\-host
+.RI [ user @] host [: port ]
+.PD 0
+.TP
+.B \-H
+.RI \(dq[ user @] host [: port ]
+[
+.RI [ user @] host [: port
+] ... ]\(dq
+.PD 0
+.TP
+.B \-\-host
+.RI \(dq[ user @] host [: port ]
+[
+.RI [ user @] host [: port
+] ... ]\(dq
+.PD 0
+.IP
+Add the given host strings to the list of hosts. This option may be given
+multiple times, and may be used in conjunction with the
+.B \-h
+option.
+
+.TP
+.BI \-l " user"
+.PD 0
+.TP
+.BI \-\-user " user"
+Use the given username as the default for any host entries that don't
+specifically specify a user.
+
+.TP
+.BI \-p " parallelism"
+.PD 0
+.TP
+.BI \-\-par " parallelism"
+Use the given number as the maximum number of concurrent connections.
+
+.TP
+.BI \-t " timeout"
+.PD 0
+.TP
+.BI \-\-timeout " timeout"
+Make connections time out after the given number of seconds. With a value
+of 0, pssh will not timeout any connections.
+
+.TP
+.BI \-o " outdir"
+.PD 0
+.TP
+.BI \-\-outdir " outdir"
+Save standard output to files in the given directory. Filenames are of the
+form
+.RI [ user @] host [: port ][. num ]
+where the user and port are only included for hosts that explicitly
+specify them. The number is a counter that is incremented each time for hosts
+that are specified more than once.
+
+.TP
+.BI \-e " errdir"
+.PD 0
+.TP
+.BI \-\-errdir " errdir"
+Save standard error to files in the given directory. Filenames are of the
+same form as with the
+.B \-o
+option.
+
+.TP
+.BI \-x " args"
+.PD 0
+.TP
+.BI \-\-extra-args " args"
+Passes a extra SSH command-line arguments (see the
+.BR ssh (1)
+man page for more information about SSH arguments).
+This option may be specified multiple times.
+The arguments are processed to split on whitespace, protect text within
+quotes, and escape with backslashes.
+To pass arguments without such processing, use the
+.B \-X
+option instead.
+
+.TP
+.BI \-X " arg"
+.PD 0
+.TP
+.BI \-\-extra-arg " arg"
+Passes a single SSH command-line argument (see the
+.BR ssh (1)
+man page for more information about SSH arguments). Unlike the
+.B \-x
+option, no processing is performed on the argument, including word splitting.
+To pass multiple command-line arguments, use the option once for each
+argument.
+
+.TP
+.BI \-O " options"
+.PD 0
+.TP
+.BI \-\-options " options"
+SSH options in the format used in the SSH configuration file (see the
+.BR ssh_config (5)
+man page for more information). This option may be specified multiple
+times.
+
+.TP
+.B \-A
+.PD 0
+.TP
+.B \-\-askpass
+Prompt for a password and pass it to ssh. The password may be used for
+either to unlock a key or for password authentication.
+The password is transferred in a fairly secure manner (e.g., it will not show
+up in argument lists). However, be aware that a root user on your system
+could potentially intercept the password.
+
+.TP
+.B \-i
+.PD 0
+.TP
+.B \-\-inline
+Display standard output and standard error as each host completes.
+
+.TP
+.B \-v
+.PD 0
+.TP
+.B \-\-verbose
+Include error messages from ssh with the
+.B \-i
+and
+.B \e
+options.
+
+.TP
+.B \-I
+.PD 0
+.TP
+.B \-\-send-input
+Read input and send to each ssh process. Since ssh allows a command script to
+be sent on standard input, the
+.B \-I
+option may be used in lieu of the command argument.
+
+.TP
+.B \-P
+.PD 0
+.TP
+.B \-\-print
+Display output as it arrives. This option is of limited usefulness because
+output from different hosts are interleaved.
+
+
+.SH EXAMPLE
+
+.PP
+Connect to host1 and host2, and print "hello, world" from each:
+.RS
+pssh -i -H "host1 host2" echo "hello, world"
+.RE
+
+.PP
+Print "hello, world" from each host specified in the file hosts.txt:
+.RS
+pssh -i -h hosts.txt echo "hello, world"
+.RE
+
+.PP
+Run a command as root with a prompt for the root password:
+.RS
+pssh -i -h hosts.txt -A -l root echo hi
+.RE
+
+.PP
+Run a long command without timing out:
+.RS
+pssh -i -h hosts.txt -t 0 sleep 10000
+.RE
+
+.PP
+If the file hosts.txt has a large number of entries, say 100, then the
+parallelism option may also be set to 100 to ensure that the commands are run
+concurrently:
+.RS
+pssh -i -h hosts.txt -p 100 -t 0 sleep 10000
+.RE
+
+.PP
+Run a command without checking or saving host keys:
+.RS
+pssh -i -H host1 -H host2 -x "-O StrictHostKeyChecking=no -O UserKnownHostsFile=/dev/null -O GlobalKnownHostsFile=/dev/null" echo hi
+.RE
+
+
+.SH EXIT STATUS VALUES
+.PP
+
+.TP
+.B 0
+Success
+
+.TP
+.B 1
+Miscellaneous error
+
+.TP
+.B 2
+Syntax or usage error
+
+.TP
+.B 3
+At least one process was killed by a signal or timed out.
+
+.TP
+.B 4
+All processes completed, but at least one ssh process reported an error
+(exit status 255).
+
+.TP
+.B 5
+There were no ssh errors, but at least one remote command had a non-zero exit
+status.
+
+
+.SH AUTHORS
+.PP
+Written by
+Brent N. Chun <bnc@theether.org> and
+Andrew McNabb <amcnabb@mcnabbs.org>.
+
+http://code.google.com/p/parallel-ssh/
+
+
+.SH SEE ALSO
+.BR ssh (1),
+.BR pscp (1),
+.BR prsync (1),
+.BR pslurp (1),
+.BR pnuke (1)
diff --git a/planetlab/pssh/psshlib/__init__.py b/planetlab/pssh/psshlib/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/planetlab/pssh/psshlib/__init__.py
diff --git a/planetlab/pssh/psshlib/askpass_client.py b/planetlab/pssh/psshlib/askpass_client.py
new file mode 100644
index 0000000..fa4d40a
--- /dev/null
+++ b/planetlab/pssh/psshlib/askpass_client.py
@@ -0,0 +1,95 @@
+#!/usr/bin/env python
+# -*- Mode: python -*-
+
+# Copyright (c) 2009, Andrew McNabb
+
+"""Implementation of SSH_ASKPASS to get a password to ssh from pssh.
+
+The password is read from the socket specified by the environment variable
+PSSH_ASKPASS_SOCKET. The other end of this socket is pssh.
+
+The ssh man page discusses SSH_ASKPASS as follows:
+ If ssh needs a passphrase, it will read the passphrase from the current
+ terminal if it was run from a terminal. If ssh does not have a terminal
+ associated with it but DISPLAY and SSH_ASKPASS are set, it will execute
+ the program specified by SSH_ASKPASS and open an X11 window to read the
+ passphrase. This is particularly useful when calling ssh from a .xsession
+ or related script. (Note that on some machines it may be necessary to
+ redirect the input from /dev/null to make this work.)
+"""
+
+import os
+import socket
+import sys
+import textwrap
+
+bin_dir = os.path.dirname(os.path.abspath(sys.argv[0]))
+askpass_bin_path = os.path.join(bin_dir, 'pssh-askpass')
+ASKPASS_PATHS = (askpass_bin_path,
+ '/usr/libexec/pssh/pssh-askpass',
+ '/usr/local/libexec/pssh/pssh-askpass',
+ '/usr/lib/pssh/pssh-askpass',
+ '/usr/local/lib/pssh/pssh-askpass')
+
+_executable_path = None
+
+def executable_path():
+ """Determines the value to use for SSH_ASKPASS.
+
+ The value is cached since this may be called many times.
+ """
+ global _executable_path
+ if _executable_path is None:
+ for path in ASKPASS_PATHS:
+ if os.access(path, os.X_OK):
+ _executable_path = path
+ break
+ else:
+ _executable_path = ''
+ sys.stderr.write(textwrap.fill("Warning: could not find an"
+ " executable path for askpass because PSSH was not"
+ " installed correctly. Password prompts will not work."))
+ sys.stderr.write('\n')
+ return _executable_path
+
+def askpass_main():
+ """Connects to pssh over the socket specified at PSSH_ASKPASS_SOCKET."""
+
+ # It's not documented anywhere, as far as I can tell, but ssh may prompt
+ # for a password or ask a yes/no question. The command-line argument
+ # specifies what is needed.
+ if len(sys.argv) > 1:
+ prompt = sys.argv[1]
+ if not prompt.lower().endswith('password: '):
+ sys.stderr.write(prompt)
+ sys.stderr.write('\n')
+ sys.exit(1)
+
+ address = os.getenv('PSSH_ASKPASS_SOCKET')
+ if not address:
+ sys.stderr.write(textwrap.fill("pssh error: SSH requested a password."
+ " Please create SSH keys or use the -A option to provide a"
+ " password."))
+ sys.stderr.write('\n')
+ sys.exit(1)
+
+ sock = socket.socket(socket.AF_UNIX)
+ try:
+ sock.connect(address)
+ except socket.error:
+ _, e, _ = sys.exc_info()
+ message = e.args[1]
+ sys.stderr.write("Couldn't bind to %s: %s.\n" % (address, message))
+ sys.exit(2)
+
+ try:
+ password = sock.makefile().read()
+ except socket.error:
+ sys.stderr.write("Socket error.\n")
+ sys.exit(3)
+
+ print(password)
+
+
+if __name__ == '__main__':
+ askpass_main()
diff --git a/planetlab/pssh/psshlib/askpass_server.py b/planetlab/pssh/psshlib/askpass_server.py
new file mode 100644
index 0000000..a5db977
--- /dev/null
+++ b/planetlab/pssh/psshlib/askpass_server.py
@@ -0,0 +1,101 @@
+#!/usr/bin/env python
+# -*- Mode: python -*-
+
+# Copyright (c) 2009, Andrew McNabb
+
+"""Sends the password over a socket to askpass.
+"""
+
+import errno
+import getpass
+import os
+import socket
+import sys
+import tempfile
+import textwrap
+
+from psshlib import psshutil
+
+
+class PasswordServer(object):
+ """Listens on a UNIX domain socket for password requests."""
+ def __init__(self):
+ self.sock = None
+ self.tempdir = None
+ self.address = None
+ self.socketmap = {}
+ self.buffermap = {}
+
+ def start(self, iomap, backlog):
+ """Prompts for the password, creates a socket, and starts listening.
+
+ The specified backlog should be the max number of clients connecting
+ at once.
+ """
+ message = ('Warning: do not enter your password if anyone else has'
+ ' superuser privileges or access to your account.')
+ print(textwrap.fill(message))
+
+ self.password = getpass.getpass()
+
+ # Note that according to the docs for mkdtemp, "The directory is
+ # readable, writable, and searchable only by the creating user."
+ self.tempdir = tempfile.mkdtemp(prefix='pssh.')
+ self.address = os.path.join(self.tempdir, 'pssh_askpass_socket')
+ self.sock = socket.socket(socket.AF_UNIX)
+ psshutil.set_cloexec(self.sock)
+ self.sock.bind(self.address)
+ self.sock.listen(backlog)
+ iomap.register_read(self.sock.fileno(), self.handle_listen)
+
+ def handle_listen(self, fd, iomap):
+ try:
+ conn = self.sock.accept()[0]
+ except socket.error:
+ _, e, _ = sys.exc_info()
+ number = e.args[0]
+ if number == errno.EINTR:
+ return
+ else:
+ # TODO: print an error message here?
+ self.sock.close()
+ self.sock = None
+ fd = conn.fileno()
+ iomap.register_write(fd, self.handle_write)
+ self.socketmap[fd] = conn
+ self.buffermap[fd] = self.password
+
+ def handle_write(self, fd, iomap):
+ buffer = self.buffermap[fd]
+ conn = self.socketmap[fd]
+ try:
+ bytes_written = conn.send(buffer)
+ except socket.error:
+ _, e, _ = sys.exc_info()
+ number = e.args[0]
+ if number == errno.EINTR:
+ return
+ else:
+ self.close_socket(fd, iomap)
+
+ buffer = buffer[bytes_written:]
+ if buffer:
+ self.buffermap[fd] = buffer
+ else:
+ self.close_socket(fd, iomap)
+
+ def close_socket(self, fd, iomap):
+ iomap.unregister(fd)
+ self.socketmap[fd].close()
+ del self.socketmap[fd]
+ del self.buffermap[fd]
+
+ def __del__(self):
+ if self.sock:
+ self.sock.close()
+ self.sock = None
+ if self.address:
+ os.remove(self.address)
+ if self.tempdir:
+ os.rmdir(self.tempdir)
+
diff --git a/planetlab/pssh/psshlib/cli.py b/planetlab/pssh/psshlib/cli.py
new file mode 100644
index 0000000..1686ba9
--- /dev/null
+++ b/planetlab/pssh/psshlib/cli.py
@@ -0,0 +1,108 @@
+# Copyright (c) 2009, Andrew McNabb
+# Copyright (c) 2003-2008, Brent N. Chun
+
+import optparse
+import os
+import shlex
+import sys
+import textwrap
+
+_DEFAULT_PARALLELISM = 32
+_DEFAULT_TIMEOUT = 0 # "infinity" by default
+
+
+def common_parser():
+ """
+ Create a basic OptionParser with arguments common to all pssh programs.
+ """
+ # The "resolve" conflict handler avoids errors from the hosts option
+ # conflicting with the help option.
+ parser = optparse.OptionParser(conflict_handler='resolve')
+ # Ensure that options appearing after the command are sent to ssh.
+ parser.disable_interspersed_args()
+ parser.epilog = "Example: pssh -h nodes.txt -l irb2 -o /tmp/foo uptime"
+
+ parser.add_option('-h', '--hosts', dest='host_files', action='append',
+ metavar='HOST_FILE',
+ help='hosts file (each line "[user@]host[:port]")')
+ parser.add_option('-H', '--host', dest='host_strings', action='append',
+ metavar='HOST_STRING',
+ help='additional host entries ("[user@]host[:port]")')
+ parser.add_option('-l', '--user', dest='user',
+ help='username (OPTIONAL)')
+ parser.add_option('-p', '--par', dest='par', type='int',
+ help='max number of parallel threads (OPTIONAL)')
+ parser.add_option('-o', '--outdir', dest='outdir',
+ help='output directory for stdout files (OPTIONAL)')
+ parser.add_option('-e', '--errdir', dest='errdir',
+ help='output directory for stderr files (OPTIONAL)')
+ parser.add_option('-t', '--timeout', dest='timeout', type='int',
+ help='timeout (secs) (0 = no timeout) per host (OPTIONAL)')
+ parser.add_option('-O', '--option', dest='options', action='append',
+ metavar='OPTION', help='SSH option (OPTIONAL)')
+ parser.add_option('-v', '--verbose', dest='verbose', action='store_true',
+ help='turn on warning and diagnostic messages (OPTIONAL)')
+ parser.add_option('-A', '--askpass', dest='askpass', action='store_true',
+ help='Ask for a password (OPTIONAL)')
+ parser.add_option('-x', '--extra-args', action='callback', type='string',
+ metavar='ARGS', callback=shlex_append, dest='extra',
+ help='Extra command-line arguments, with processing for '
+ 'spaces, quotes, and backslashes')
+ parser.add_option('-X', '--extra-arg', dest='extra', action='append',
+ metavar='ARG', help='Extra command-line argument')
+
+ return parser
+
+
+def common_defaults(**kwargs):
+ defaults = dict(par=_DEFAULT_PARALLELISM, timeout=_DEFAULT_TIMEOUT)
+ defaults.update(**kwargs)
+ envvars = [('user', 'PSSH_USER'),
+ ('par', 'PSSH_PAR'),
+ ('outdir', 'PSSH_OUTDIR'),
+ ('errdir', 'PSSH_ERRDIR'),
+ ('timeout', 'PSSH_TIMEOUT'),
+ ('verbose', 'PSSH_VERBOSE'),
+ ('print_out', 'PSSH_PRINT'),
+ ('askpass', 'PSSH_ASKPASS'),
+ ('inline', 'PSSH_INLINE'),
+ ('recursive', 'PSSH_RECURSIVE'),
+ ('archive', 'PSSH_ARCHIVE'),
+ ('compress', 'PSSH_COMPRESS'),
+ ('localdir', 'PSSH_LOCALDIR'),
+ ]
+ for option, var, in envvars:
+ value = os.getenv(var)
+ if value:
+ defaults[option] = value
+
+ value = os.getenv('PSSH_OPTIONS')
+ if value:
+ defaults['options'] = [value]
+
+ value = os.getenv('PSSH_HOSTS')
+ if value:
+ message1 = ('Warning: the PSSH_HOSTS environment variable is '
+ 'deprecated. Please use the "-h" option instead, and consider '
+ 'creating aliases for convenience. For example:')
+ message2 = " alias pssh_abc='pssh -h /path/to/hosts_abc'"
+ sys.stderr.write(textwrap.fill(message1))
+ sys.stderr.write('\n')
+ sys.stderr.write(message2)
+ sys.stderr.write('\n')
+ defaults['host_files'] = [value]
+
+ return defaults
+
+
+def shlex_append(option, opt_str, value, parser):
+ """An optparse callback similar to the append action.
+
+ The given value is processed with shlex, and the resulting list is
+ concatenated to the option's dest list.
+ """
+ lst = getattr(parser.values, option.dest)
+ if lst is None:
+ lst = []
+ setattr(parser.values, option.dest, lst)
+ lst.extend(shlex.split(value))
diff --git a/planetlab/pssh/psshlib/color.py b/planetlab/pssh/psshlib/color.py
new file mode 100644
index 0000000..eb9f001
--- /dev/null
+++ b/planetlab/pssh/psshlib/color.py
@@ -0,0 +1,39 @@
+# Copyright (c) 2009, Andrew McNabb
+# Copyright (c) 2003-2008, Brent N. Chun
+
+def with_color(string, fg, bg=49):
+ '''Given foreground/background ANSI color codes, return a string that,
+ when printed, will format the supplied string using the supplied colors.
+ '''
+ return "\x1b[%dm\x1b[%dm%s\x1b[39m\x1b[49m" % (fg, bg, string)
+
+def B(string):
+ '''Returns a string that, when printed, will display the supplied string
+ in ANSI bold.
+ '''
+ return "\x1b[1m%s\x1b[22m" % string
+
+def r(string): return with_color(string, 31) # Red
+def g(string): return with_color(string, 32) # Green
+def y(string): return with_color(string, 33) # Yellow
+def b(string): return with_color(string, 34) # Blue
+def m(string): return with_color(string, 35) # Magenta
+def c(string): return with_color(string, 36) # Cyan
+def w(string): return with_color(string, 37) # White
+
+#following from Python cookbook, #475186
+def has_colors(stream):
+ '''Returns boolean indicating whether or not the supplied stream supports
+ ANSI color.
+ '''
+ if not hasattr(stream, "isatty"):
+ return False
+ if not stream.isatty():
+ return False # auto color only on TTYs
+ try:
+ import curses
+ curses.setupterm()
+ return curses.tigetnum("colors") > 2
+ except:
+ # guess false in case of error
+ return False
diff --git a/planetlab/pssh/psshlib/manager.py b/planetlab/pssh/psshlib/manager.py
new file mode 100644
index 0000000..b10959d
--- /dev/null
+++ b/planetlab/pssh/psshlib/manager.py
@@ -0,0 +1,345 @@
+# Copyright (c) 2009, Andrew McNabb
+
+from errno import EINTR
+import os
+import select
+import signal
+import sys
+import threading
+
+try:
+ import queue
+except ImportError:
+ import Queue as queue
+
+from psshlib.askpass_server import PasswordServer
+from psshlib import psshutil
+
+READ_SIZE = 1 << 16
+
+
+class FatalError(RuntimeError):
+ """A fatal error in the PSSH Manager."""
+ pass
+
+
+class Manager(object):
+ """Executes tasks concurrently.
+
+ Tasks are added with add_task() and executed in parallel with run().
+ Returns a list of the exit statuses of the processes.
+
+ Arguments:
+ limit: Maximum number of commands running at once.
+ timeout: Maximum allowed execution time in seconds.
+ """
+ def __init__(self, opts):
+ self.limit = opts.par
+ self.timeout = opts.timeout
+ self.askpass = opts.askpass
+ self.outdir = opts.outdir
+ self.errdir = opts.errdir
+ self.iomap = IOMap()
+
+ self.taskcount = 0
+ self.tasks = []
+ self.running = []
+ self.done = []
+
+ self.askpass_socket = None
+
+ def run(self):
+ """Processes tasks previously added with add_task."""
+ try:
+ if self.outdir or self.errdir:
+ writer = Writer(self.outdir, self.errdir)
+ writer.start()
+ else:
+ writer = None
+
+ if self.askpass:
+ pass_server = PasswordServer()
+ pass_server.start(self.iomap, self.limit)
+ self.askpass_socket = pass_server.address
+
+ self.set_sigchld_handler()
+
+ try:
+ self.update_tasks(writer)
+ wait = None
+ while self.running or self.tasks:
+ # Opt for efficiency over subsecond timeout accuracy.
+ if wait is None or wait < 1:
+ wait = 1
+ self.iomap.poll(wait)
+ self.update_tasks(writer)
+ wait = self.check_timeout()
+ except KeyboardInterrupt:
+ # This exception handler tries to clean things up and prints
+ # out a nice status message for each interrupted host.
+ self.interrupted()
+
+ except KeyboardInterrupt:
+ # This exception handler doesn't print out any fancy status
+ # information--it just stops.
+ pass
+
+ if writer:
+ writer.signal_quit()
+ writer.join()
+
+ statuses = [task.exitstatus for task in self.done]
+ return statuses
+
+ def clear_sigchld_handler(self):
+ signal.signal(signal.SIGCHLD, signal.SIG_DFL)
+
+ def set_sigchld_handler(self):
+ # TODO: find out whether set_wakeup_fd still works if the default
+ # signal handler is used (I'm pretty sure it doesn't work if the
+ # signal is ignored).
+ signal.signal(signal.SIGCHLD, self.handle_sigchld)
+ # This should keep reads and writes from getting EINTR.
+ if hasattr(signal, 'siginterrupt'):
+ signal.siginterrupt(signal.SIGCHLD, False)
+
+ def handle_sigchld(self, number, frame):
+ """Apparently we need a sigchld handler to make set_wakeup_fd work."""
+ # Write to the signal pipe (only for Python <2.5, where the
+ # set_wakeup_fd method doesn't exist).
+ if self.iomap.wakeup_writefd:
+ os.write(self.iomap.wakeup_writefd, '\0')
+ for task in self.running:
+ if task.proc:
+ task.proc.poll()
+ # Apparently some UNIX systems automatically resent the SIGCHLD
+ # handler to SIG_DFL. Reset it just in case.
+ self.set_sigchld_handler()
+
+ def add_task(self, task):
+ """Adds a Task to be processed with run()."""
+ self.tasks.append(task)
+
+ def update_tasks(self, writer):
+ """Reaps tasks and starts as many new ones as allowed."""
+ # Mask signals to work around a Python bug:
+ # http://bugs.python.org/issue1068268
+ # Since sigprocmask isn't in the stdlib, clear the SIGCHLD handler.
+ # Since signals are masked, reap_tasks needs to be called once for
+ # each loop.
+ keep_running = True
+ while keep_running:
+ self.clear_sigchld_handler()
+ self._start_tasks_once(writer)
+ self.set_sigchld_handler()
+ keep_running = self.reap_tasks()
+
+ def _start_tasks_once(self, writer):
+ """Starts tasks once.
+
+ Due to http://bugs.python.org/issue1068268, signals must be masked
+ when this method is called.
+ """
+ while 0 < len(self.tasks) and len(self.running) < self.limit:
+ task = self.tasks.pop(0)
+ self.running.append(task)
+ task.start(self.taskcount, self.iomap, writer, self.askpass_socket)
+ self.taskcount += 1
+
+ def reap_tasks(self):
+ """Checks to see if any tasks have terminated.
+
+ After cleaning up, returns the number of tasks that finished.
+ """
+ still_running = []
+ finished_count = 0
+ for task in self.running:
+ if task.running():
+ still_running.append(task)
+ else:
+ self.finished(task)
+ finished_count += 1
+ self.running = still_running
+ return finished_count
+
+ def check_timeout(self):
+ """Kills timed-out processes and returns the lowest time left."""
+ if self.timeout <= 0:
+ return None
+
+ min_timeleft = None
+ for task in self.running:
+ timeleft = self.timeout - task.elapsed()
+ if timeleft <= 0:
+ task.timedout()
+ continue
+ if min_timeleft is None or timeleft < min_timeleft:
+ min_timeleft = timeleft
+
+ if min_timeleft is None:
+ return 0
+ else:
+ return max(0, min_timeleft)
+
+ def interrupted(self):
+ """Cleans up after a keyboard interrupt."""
+ for task in self.running:
+ task.interrupted()
+ self.finished(task)
+
+ for task in self.tasks:
+ task.cancel()
+ self.finished(task)
+
+ def finished(self, task):
+ """Marks a task as complete and reports its status to stdout."""
+ self.done.append(task)
+ n = len(self.done)
+ task.report(n)
+
+
+class IOMap(object):
+ """A manager for file descriptors and their associated handlers.
+
+ The poll method dispatches events to the appropriate handlers.
+ """
+ def __init__(self):
+ self.readmap = {}
+ self.writemap = {}
+
+ # Setup the wakeup file descriptor to avoid hanging on lost signals.
+ wakeup_readfd, wakeup_writefd = os.pipe()
+ self.register_read(wakeup_readfd, self.wakeup_handler)
+ # TODO: remove test when we stop supporting Python <2.5
+ if hasattr(signal, 'set_wakeup_fd'):
+ signal.set_wakeup_fd(wakeup_writefd)
+ self.wakeup_writefd = None
+ else:
+ self.wakeup_writefd = wakeup_writefd
+
+ def register_read(self, fd, handler):
+ """Registers an IO handler for a file descriptor for reading."""
+ self.readmap[fd] = handler
+
+ def register_write(self, fd, handler):
+ """Registers an IO handler for a file descriptor for writing."""
+ self.writemap[fd] = handler
+
+ def unregister(self, fd):
+ """Unregisters the given file descriptor."""
+ if fd in self.readmap:
+ del self.readmap[fd]
+ if fd in self.writemap:
+ del self.writemap[fd]
+
+ def poll(self, timeout=None):
+ """Performs a poll and dispatches the resulting events."""
+ if not self.readmap and not self.writemap:
+ return
+ rlist = list(self.readmap)
+ wlist = list(self.writemap)
+ try:
+ rlist, wlist, _ = select.select(rlist, wlist, [], timeout)
+ except select.error:
+ _, e, _ = sys.exc_info()
+ errno = e.args[0]
+ if errno == EINTR:
+ return
+ else:
+ raise
+ for fd in rlist:
+ handler = self.readmap[fd]
+ handler(fd, self)
+ for fd in wlist:
+ handler = self.writemap[fd]
+ handler(fd, self)
+
+ def wakeup_handler(self, fd, iomap):
+ """Handles read events on the signal wakeup pipe.
+
+ This ensures that SIGCHLD signals aren't lost.
+ """
+ try:
+ os.read(fd, READ_SIZE)
+ except (OSError, IOError):
+ _, e, _ = sys.exc_info()
+ errno, message = e.args
+ if errno != EINTR:
+ sys.stderr.write('Fatal error reading from wakeup pipe: %s\n'
+ % message)
+ raise FatalError
+
+
+class Writer(threading.Thread):
+ """Thread that writes to files by processing requests from a Queue.
+
+ Until AIO becomes widely available, it is impossible to make a nonblocking
+ write to an ordinary file. The Writer thread processes all writing to
+ ordinary files so that the main thread can work without blocking.
+ """
+ OPEN = object()
+ EOF = object()
+ ABORT = object()
+
+ def __init__(self, outdir, errdir):
+ threading.Thread.__init__(self)
+ # A daemon thread automatically dies if the program is terminated.
+ self.setDaemon(True)
+ self.queue = queue.Queue()
+ self.outdir = outdir
+ self.errdir = errdir
+
+ self.host_counts = {}
+ self.files = {}
+
+ def run(self):
+ while True:
+ filename, data = self.queue.get()
+ if filename == self.ABORT:
+ return
+
+ if data == self.OPEN:
+ self.files[filename] = open(filename, 'wb', buffering=1)
+ psshutil.set_cloexec(self.files[filename])
+ else:
+ dest = self.files[filename]
+ if data == self.EOF:
+ dest.close()
+ else:
+ dest.write(data)
+
+ def open_files(self, host):
+ """Called from another thread to create files for stdout and stderr.
+
+ Returns a pair of filenames (outfile, errfile). These filenames are
+ used as handles for future operations. Either or both may be None if
+ outdir or errdir or not set.
+ """
+ outfile = errfile = None
+ if self.outdir or self.errdir:
+ count = self.host_counts.get(host, 0)
+ self.host_counts[host] = count + 1
+ if count:
+ filename = "%s.%s" % (host, count)
+ else:
+ filename = host
+ if self.outdir:
+ outfile = os.path.join(self.outdir, filename)
+ self.queue.put((outfile, self.OPEN))
+ if self.errdir:
+ errfile = os.path.join(self.errdir, filename)
+ self.queue.put((errfile, self.OPEN))
+ return outfile, errfile
+
+ def write(self, filename, data):
+ """Called from another thread to enqueue a write."""
+ self.queue.put((filename, data))
+
+ def close(self, filename):
+ """Called from another thread to close the given file."""
+ self.queue.put((filename, self.EOF))
+
+ def signal_quit(self):
+ """Called from another thread to request the Writer to quit."""
+ self.queue.put((self.ABORT, None))
+
diff --git a/planetlab/pssh/psshlib/psshutil.py b/planetlab/pssh/psshlib/psshutil.py
new file mode 100644
index 0000000..ae1a24c
--- /dev/null
+++ b/planetlab/pssh/psshlib/psshutil.py
@@ -0,0 +1,108 @@
+# Copyright (c) 2009, Andrew McNabb
+# Copyright (c) 2003-2008, Brent N. Chun
+
+import fcntl
+import string
+import sys
+
+HOST_FORMAT = 'Host format is [user@]host[:port] [user]'
+
+
+def read_host_files(paths, default_user=None, default_port=None):
+ """Reads the given host files.
+
+ Returns a list of (host, port, user) triples.
+ """
+ hosts = []
+ if paths:
+ for path in paths:
+ hosts.extend(read_host_file(path, default_user=default_user))
+ return hosts
+
+
+def read_host_file(path, default_user=None, default_port=None):
+ """Reads the given host file.
+
+ Lines are of the form: host[:port] [login].
+ Returns a list of (host, port, user) triples.
+ """
+ lines = []
+ f = open(path)
+ for line in f:
+ lines.append(line.strip())
+ f.close()
+
+ hosts = []
+ for line in lines:
+ # Skip blank lines or lines starting with #
+ line = line.strip()
+ if not line or line.startswith('#'):
+ continue
+ host, port, user = parse_host_entry(line, default_user, default_port)
+ if host:
+ hosts.append((host, port, user))
+ return hosts
+
+
+# TODO: deprecate the second host field and standardize on the
+# [user@]host[:port] format.
+def parse_host_entry(line, default_user, default_port):
+ """Parses a single host entry.
+
+ This may take either the of the form [user@]host[:port] or
+ host[:port][ user].
+
+ Returns a (host, port, user) triple.
+ """
+ fields = line.split()
+ if len(fields) > 2:
+ sys.stderr.write('Bad line: "%s". Format should be'
+ ' [user@]host[:port] [user]\n' % line)
+ return None, None, None
+ host_field = fields[0]
+ host, port, user = parse_host(host_field, default_port=default_port)
+ if len(fields) == 2:
+ if user is None:
+ user = fields[1]
+ else:
+ sys.stderr.write('User specified twice in line: "%s"\n' % line)
+ return None, None, None
+ if user is None:
+ user = default_user
+ return host, port, user
+
+
+def parse_host_string(host_string, default_user=None, default_port=None):
+ """Parses a whitespace-delimited string of "[user@]host[:port]" entries.
+
+ Returns a list of (host, port, user) triples.
+ """
+ hosts = []
+ entries = host_string.split()
+ for entry in entries:
+ hosts.append(parse_host(entry, default_user, default_port))
+ return hosts
+
+
+def parse_host(host, default_user=None, default_port=None):
+ """Parses host entries of the form "[user@]host[:port]".
+
+ Returns a (host, port, user) triple.
+ """
+ # TODO: when we stop supporting Python 2.4, switch to using str.partition.
+ user = default_user
+ port = default_port
+ if '@' in host:
+ user, host = host.split('@', 1)
+ if ':' in host:
+ host, port = host.rsplit(':', 1)
+ return (host, port, user)
+
+
+def set_cloexec(filelike):
+ """Sets the underlying filedescriptor to automatically close on exec.
+
+ If set_cloexec is called for all open files, then subprocess.Popen does
+ not require the close_fds option.
+ """
+ fcntl.fcntl(filelike.fileno(), fcntl.FD_CLOEXEC, 1)
diff --git a/planetlab/pssh/psshlib/task.py b/planetlab/pssh/psshlib/task.py
new file mode 100644
index 0000000..d2ac132
--- /dev/null
+++ b/planetlab/pssh/psshlib/task.py
@@ -0,0 +1,281 @@
+# Copyright (c) 2009, Andrew McNabb
+
+from errno import EINTR
+from subprocess import Popen, PIPE
+import os
+import signal
+import sys
+import time
+import traceback
+
+from psshlib import askpass_client
+from psshlib import color
+
+BUFFER_SIZE = 1 << 16
+
+try:
+ bytes
+except NameError:
+ bytes = str
+
+
+class Task(object):
+ """Starts a process and manages its input and output.
+
+ Upon completion, the `exitstatus` attribute is set to the exit status
+ of the process.
+ """
+ def __init__(self, host, port, user, cmd, opts, stdin=None):
+ self.exitstatus = None
+
+ self.host = host
+ self.pretty_host = host
+ self.port = port
+ self.cmd = cmd
+
+ if user != opts.user:
+ self.pretty_host = '@'.join((user, self.pretty_host))
+ if port:
+ self.pretty_host = ':'.join((self.pretty_host, port))
+
+ self.proc = None
+ self.writer = None
+ self.timestamp = None
+ self.failures = []
+ self.killed = False
+ self.inputbuffer = stdin
+ self.byteswritten = 0
+ self.outputbuffer = bytes()
+ self.errorbuffer = bytes()
+
+ self.stdin = None
+ self.stdout = None
+ self.stderr = None
+ self.outfile = None
+ self.errfile = None
+
+ # Set options.
+ self.verbose = opts.verbose
+ try:
+ self.print_out = bool(opts.print_out)
+ except AttributeError:
+ self.print_out = False
+ try:
+ self.inline = bool(opts.inline)
+ except AttributeError:
+ self.inline = False
+
+ def start(self, nodenum, iomap, writer, askpass_socket=None):
+ """Starts the process and registers files with the IOMap."""
+ self.writer = writer
+
+ if writer:
+ self.outfile, self.errfile = writer.open_files(self.pretty_host)
+
+ # Set up the environment.
+ environ = dict(os.environ)
+ environ['PSSH_NODENUM'] = str(nodenum)
+ # Disable the GNOME pop-up password dialog and allow ssh to use
+ # askpass.py to get a provided password. If the module file is
+ # askpass.pyc, we replace the extension.
+ environ['SSH_ASKPASS'] = askpass_client.executable_path()
+ if askpass_socket:
+ environ['PSSH_ASKPASS_SOCKET'] = askpass_socket
+ # Work around a mis-feature in ssh where it won't call SSH_ASKPASS
+ # if DISPLAY is unset.
+ if 'DISPLAY' not in environ:
+ environ['DISPLAY'] = 'pssh-gibberish'
+
+ # Create the subprocess. Since we carefully call set_cloexec() on
+ # all open files, we specify close_fds=False.
+ self.proc = Popen(self.cmd, stdin=PIPE, stdout=PIPE, stderr=PIPE,
+ close_fds=False, preexec_fn=os.setsid, env=environ)
+ self.timestamp = time.time()
+ if self.inputbuffer:
+ self.stdin = self.proc.stdin
+ iomap.register_write(self.stdin.fileno(), self.handle_stdin)
+ else:
+ self.proc.stdin.close()
+ self.stdout = self.proc.stdout
+ iomap.register_read(self.stdout.fileno(), self.handle_stdout)
+ self.stderr = self.proc.stderr
+ iomap.register_read(self.stderr.fileno(), self.handle_stderr)
+
+ def _kill(self):
+ """Signals the process to terminate."""
+ if self.proc:
+ try:
+ os.kill(-self.proc.pid, signal.SIGKILL)
+ except OSError:
+ # If the kill fails, then just assume the process is dead.
+ pass
+ self.killed = True
+
+ def timedout(self):
+ """Kills the process and registers a timeout error."""
+ if not self.killed:
+ self._kill()
+ self.failures.append('Timed out')
+
+ def interrupted(self):
+ """Kills the process and registers an keyboard interrupt error."""
+ if not self.killed:
+ self._kill()
+ self.failures.append('Interrupted')
+
+ def cancel(self):
+ """Stops a task that has not started."""
+ self.failures.append('Cancelled')
+
+ def elapsed(self):
+ """Finds the time in seconds since the process was started."""
+ return time.time() - self.timestamp
+
+ def running(self):
+ """Finds if the process has terminated and saves the return code."""
+ if self.stdin or self.stdout or self.stderr:
+ return True
+ if self.proc:
+ self.exitstatus = self.proc.poll()
+ if self.exitstatus is None:
+ if self.killed:
+ # Set the exitstatus to what it would be if we waited.
+ self.exitstatus = -signal.SIGKILL
+ return False
+ else:
+ return True
+ else:
+ if self.exitstatus < 0:
+ message = 'Killed by signal %s' % (-self.exitstatus)
+ self.failures.append(message)
+ elif self.exitstatus > 0:
+ message = 'Exited with error code %s' % self.exitstatus
+ self.failures.append(message)
+ self.proc = None
+ return False
+
+ def handle_stdin(self, fd, iomap):
+ """Called when the process's standard input is ready for writing."""
+ try:
+ start = self.byteswritten
+ if start < len(self.inputbuffer):
+ chunk = self.inputbuffer[start:start+BUFFER_SIZE]
+ self.byteswritten = start + os.write(fd, chunk)
+ else:
+ self.close_stdin(iomap)
+ except (OSError, IOError):
+ _, e, _ = sys.exc_info()
+ if e.errno != EINTR:
+ self.close_stdin(iomap)
+ self.log_exception(e)
+
+ def close_stdin(self, iomap):
+ if self.stdin:
+ iomap.unregister(self.stdin.fileno())
+ self.stdin.close()
+ self.stdin = None
+
+ def handle_stdout(self, fd, iomap):
+ """Called when the process's standard output is ready for reading."""
+ try:
+ buf = os.read(fd, BUFFER_SIZE)
+ if buf:
+ if self.inline:
+ self.outputbuffer += buf
+ if self.outfile:
+ self.writer.write(self.outfile, buf)
+ if self.print_out:
+ sys.stdout.write('%s: %s' % (self.host, buf))
+ if buf[-1] != '\n':
+ sys.stdout.write('\n')
+ else:
+ self.close_stdout(iomap)
+ except (OSError, IOError):
+ _, e, _ = sys.exc_info()
+ if e.errno != EINTR:
+ self.close_stdout(iomap)
+ self.log_exception(e)
+
+ def close_stdout(self, iomap):
+ if self.stdout:
+ iomap.unregister(self.stdout.fileno())
+ self.stdout.close()
+ self.stdout = None
+ if self.outfile:
+ self.writer.close(self.outfile)
+ self.outfile = None
+
+ def handle_stderr(self, fd, iomap):
+ """Called when the process's standard error is ready for reading."""
+ try:
+ buf = os.read(fd, BUFFER_SIZE)
+ if buf:
+ if self.inline:
+ self.errorbuffer += buf
+ if self.errfile:
+ self.writer.write(self.errfile, buf)
+ else:
+ self.close_stderr(iomap)
+ except (OSError, IOError):
+ _, e, _ = sys.exc_info()
+ if e.errno != EINTR:
+ self.close_stderr(iomap)
+ self.log_exception(e)
+
+ def close_stderr(self, iomap):
+ if self.stderr:
+ iomap.unregister(self.stderr.fileno())
+ self.stderr.close()
+ self.stderr = None
+ if self.errfile:
+ self.writer.close(self.errfile)
+ self.errfile = None
+
+ def log_exception(self, e):
+ """Saves a record of the most recent exception for error reporting."""
+ if self.verbose:
+ exc_type, exc_value, exc_traceback = sys.exc_info()
+ exc = ("Exception: %s, %s, %s" %
+ (exc_type, exc_value, traceback.format_tb(exc_traceback)))
+ else:
+ exc = str(e)
+ self.failures.append(exc)
+
+ def report(self, n):
+ """Pretty prints a status report after the Task completes."""
+ error = ', '.join(self.failures)
+ tstamp = time.asctime().split()[3] # Current time
+ if color.has_colors(sys.stdout):
+ progress = color.c("[%s]" % color.B(n))
+ success = color.g("[%s]" % color.B("SUCCESS"))
+ failure = color.r("[%s]" % color.B("FAILURE"))
+ stderr = color.r("Stderr: ")
+ error = color.r(color.B(error))
+ else:
+ progress = "[%s]" % n
+ success = "[SUCCESS]"
+ failure = "[FAILURE]"
+ stderr = "Stderr: "
+ host = self.pretty_host
+ if self.failures:
+ print(' '.join((progress, tstamp, failure, host, error)))
+ else:
+ print(' '.join((progress, tstamp, success, host)))
+ # NOTE: The extra flushes are to ensure that the data is output in
+ # the correct order with the C implementation of io.
+ if self.outputbuffer:
+ sys.stdout.flush()
+ try:
+ sys.stdout.buffer.write(self.outputbuffer)
+ sys.stdout.flush()
+ except AttributeError:
+ sys.stdout.write(self.outputbuffer)
+ if self.errorbuffer:
+ sys.stdout.write(stderr)
+ # Flush the TextIOWrapper before writing to the binary buffer.
+ sys.stdout.flush()
+ try:
+ sys.stdout.buffer.write(self.errorbuffer)
+ except AttributeError:
+ sys.stdout.write(self.errorbuffer)
+
diff --git a/planetlab/pssh/setup.py b/planetlab/pssh/setup.py
new file mode 100644
index 0000000..0dafa4e
--- /dev/null
+++ b/planetlab/pssh/setup.py
@@ -0,0 +1,41 @@
+from distutils.core import setup
+import os
+
+long_description = """PSSH (Parallel SSH) provides parallel versions of OpenSSH and related tools, including pssh, pscp, prsync, pnuke, and pslurp. The project includes psshlib which can be used within custom applications."""
+
+setup(
+ name = "pssh",
+ version = "2.2.2",
+ author = "Andrew McNabb",
+ author_email = "amcnabb@mcnabbs.org",
+ url = "http://code.google.com/p/parallel-ssh/",
+ description = "Parallel version of OpenSSH and related tools",
+ long_description = long_description,
+ license = "BSD",
+ platforms = ['linux'],
+
+ classifiers = [
+ "Development Status :: 5 - Production/Stable",
+ "Intended Audience :: System Administrators",
+ "License :: OSI Approved :: BSD License",
+ "Operating System :: POSIX",
+ "Programming Language :: Python",
+ "Programming Language :: Python :: 2",
+ "Programming Language :: Python :: 2.4",
+ "Programming Language :: Python :: 2.5",
+ "Programming Language :: Python :: 2.6",
+ "Programming Language :: Python :: 2.7",
+ "Programming Language :: Python :: 3",
+ "Programming Language :: Python :: 3.0",
+ "Programming Language :: Python :: 3.1",
+ "Programming Language :: Python :: 3.2",
+ "Topic :: Software Development :: Libraries :: Python Modules",
+ "Topic :: System :: Clustering",
+ "Topic :: System :: Networking",
+ "Topic :: System :: Systems Administration",
+ ],
+
+ packages=['psshlib'],
+ scripts = [os.path.join("bin", p) for p in ["pssh", "pnuke", "prsync", "pslurp", "pscp", "pssh-askpass"]],
+ data_files=[('man/man1', ['man/man1/pssh.1'])],
+ )
diff --git a/planetlab/pssh/test/test.py b/planetlab/pssh/test/test.py
new file mode 100644
index 0000000..e85b27d
--- /dev/null
+++ b/planetlab/pssh/test/test.py
@@ -0,0 +1,302 @@
+#!/usr/bin/python
+
+# Copyright (c) 2009, Andrew McNabb
+# Copyright (c) 2003-2008, Brent N. Chun
+
+import os
+import sys
+import shutil
+import tempfile
+import time
+import unittest
+
+basedir, bin = os.path.split(os.path.dirname(os.path.abspath(sys.argv[0])))
+sys.path.append("%s" % basedir)
+
+if os.getenv("TEST_HOSTS") is None:
+ raise Exception("Must define TEST_HOSTS")
+g_hosts = os.getenv("TEST_HOSTS").split()
+
+if os.getenv("TEST_USER") is None:
+ raise Exception("Must define TEST_USER")
+g_user = os.getenv("TEST_USER")
+
+class PsshTest(unittest.TestCase):
+ def setUp(self):
+ self.outDir = tempfile.mkdtemp()
+ self.errDir = tempfile.mkdtemp()
+
+ def teardown(self):
+ shutil.rmtree(self.errDir)
+ shutil.rmtree(self.outDir)
+
+ def testShortOpts(self):
+ hostsFile = tempfile.NamedTemporaryFile()
+ hostsFile.write("".join(map(lambda x: "%s\n" % x, g_hosts)))
+ hostsFile.flush()
+ cmd = "%s/bin/pssh -h %s -l %s -p 64 -o %s -e %s -t 60 -v -P -i uptime < /dev/null" % (basedir, hostsFile.name, g_user, self.outDir, self.errDir)
+ rv = os.system(cmd)
+ self.assertEqual(rv, 0)
+ for host in g_hosts:
+ stdout = open("%s/%s" % (self.outDir, host)).read()
+ self.assert_(stdout.find("load average") != -1)
+
+ def testLongOpts(self):
+ hostsFile = tempfile.NamedTemporaryFile()
+ hostsFile.write("".join(map(lambda x: "%s\n" % x, g_hosts)))
+ hostsFile.flush()
+ cmd = "%s/bin/pssh --hosts=%s --user=%s --par=64 --outdir=%s --errdir=%s --timeout=60 --verbose --print --inline uptime < /dev/null" % (basedir, hostsFile.name, g_user, self.outDir, self.errDir)
+ rv = os.system(cmd)
+ self.assertEqual(rv, 0)
+ for host in g_hosts:
+ stdout = open("%s/%s" % (self.outDir, host)).read()
+ self.assert_(stdout.find("load average") != -1)
+
+ def testStderr(self):
+ hostsFile = tempfile.NamedTemporaryFile()
+ hostsFile.write("".join(map(lambda x: "%s\n" % x, g_hosts)))
+ hostsFile.flush()
+ cmd = "%s/bin/pssh -h %s -l %s -p 64 -o %s -e %s -t 60 -v -P -i ls /foobarbaz < /dev/null" % (basedir, hostsFile.name, g_user, self.outDir, self.errDir)
+ rv = os.system(cmd)
+ self.assertEqual(rv, 0)
+ for host in g_hosts:
+ stdout = open("%s/%s" % (self.outDir, host)).read()
+ self.assertEqual(stdout, "")
+ stderr = open("%s/%s" % (self.errDir, host)).read()
+ self.assert_(stderr.find("No such file or directory") != -1)
+
+class PscpTest(unittest.TestCase):
+ def setUp(self):
+ self.outDir = tempfile.mkdtemp()
+ self.errDir = tempfile.mkdtemp()
+
+ def teardown(self):
+ shutil.rmtree(self.errDir)
+ shutil.rmtree(self.outDir)
+ try:
+ os.remove("/tmp/pssh.test")
+ except OSError:
+ pass
+
+ def testShortOpts(self):
+ for host in g_hosts:
+ cmd = "ssh %s@%s rm -rf /tmp/pssh.test" % (g_user, host)
+ rv = os.system(cmd)
+ self.assertEqual(rv, 0)
+
+ hostsFile = tempfile.NamedTemporaryFile()
+ hostsFile.write("".join(map(lambda x: "%s\n" % x, g_hosts)))
+ hostsFile.flush()
+ cmd = "%s/bin/pscp -h %s -l %s -p 64 -o %s -e %s -t 60 /etc/hosts /tmp/pssh.test < /dev/null" % (basedir, hostsFile.name, g_user, self.outDir, self.errDir)
+ rv = os.system(cmd)
+ self.assertEqual(rv, 0)
+ for host in g_hosts:
+ cmd = "ssh %s@%s cat /tmp/pssh.test" % (g_user, host)
+ data = os.popen(cmd).read()
+ self.assertEqual(data, open("/etc/hosts").read())
+
+ def testLongOpts(self):
+ for host in g_hosts:
+ cmd = "ssh %s@%s rm -rf /tmp/pssh.test" % (g_user, host)
+ rv = os.system(cmd)
+ self.assertEqual(rv, 0)
+
+ hostsFile = tempfile.NamedTemporaryFile()
+ hostsFile.write("".join(map(lambda x: "%s\n" % x, g_hosts)))
+ hostsFile.flush()
+ cmd = "%s/bin/pscp --hosts=%s --user=%s --par=64 --outdir=%s --errdir=%s --timeout=60 /etc/hosts /tmp/pssh.test < /dev/null" % (basedir, hostsFile.name, g_user, self.outDir, self.errDir)
+ rv = os.system(cmd)
+ self.assertEqual(rv, 0)
+ for host in g_hosts:
+ cmd = "ssh %s@%s cat /tmp/pssh.test" % (g_user, host)
+ data = os.popen(cmd).read()
+ self.assertEqual(data, open("/etc/hosts").read())
+
+ def testRecursive(self):
+ for host in g_hosts:
+ cmd = "ssh %s@%s rm -rf /tmp/pssh.test" % (g_user, host)
+ rv = os.system(cmd)
+ self.assertEqual(rv, 0)
+
+ hostsFile = tempfile.NamedTemporaryFile()
+ hostsFile.write("".join(map(lambda x: "%s\n" % x, g_hosts)))
+ hostsFile.flush()
+ cmd = "%s/bin/pscp -r -h %s -l %s -p 64 -o %s -e %s -t 60 /etc/init.d /tmp/pssh.test < /dev/null" % (basedir, hostsFile.name, g_user, self.outDir, self.errDir)
+ rv = os.system(cmd)
+ self.assertEqual(rv, 0)
+ files = os.popen("ls -R /etc/init.d | sed 1d | sort").read().strip()
+ for host in g_hosts:
+ cmd = "ssh %s@%s ls -R /tmp/pssh.test | sed 1d | sort" % (g_user, host)
+ data = os.popen(cmd).read().strip()
+ self.assertEqual(data, files)
+
+class PslurpTest(unittest.TestCase):
+ def setUp(self):
+ self.outDir = tempfile.mkdtemp()
+ self.errDir = tempfile.mkdtemp()
+
+ def teardown(self):
+ shutil.rmtree(self.errDir)
+ shutil.rmtree(self.outDir)
+
+ def testShortOpts(self):
+ if os.path.exists("/tmp/pssh.test"):
+ try:
+ os.remove("/tmp/pssh.test")
+ except OSError:
+ shutil.rmtree("/tmp/pssh.test")
+
+ hostsFile = tempfile.NamedTemporaryFile()
+ hostsFile.write("".join(map(lambda x: "%s\n" % x, g_hosts)))
+ hostsFile.flush()
+ cmd = "%s/bin/pslurp -L /tmp/pssh.test -h %s -l %s -p 64 -o %s -e %s -t 60 /etc/hosts hosts < /dev/null" % (basedir, hostsFile.name, g_user, self.outDir, self.errDir)
+ rv = os.system(cmd)
+ self.assertEqual(rv, 0)
+
+ for host in g_hosts:
+ cmd = "ssh %s@%s cat /etc/hosts" % (g_user, host)
+ data = os.popen(cmd).read()
+ self.assertEqual(data, open("/tmp/pssh.test/%s/hosts" % host).read())
+
+ def testLongOpts(self):
+ if os.path.exists("/tmp/pssh.test"):
+ try:
+ os.remove("/tmp/pssh.test")
+ except OSError:
+ shutil.rmtree("/tmp/pssh.test")
+
+ hostsFile = tempfile.NamedTemporaryFile()
+ hostsFile.write("".join(map(lambda x: "%s\n" % x, g_hosts)))
+ hostsFile.flush()
+ cmd = "%s/bin/pslurp --localdir=/tmp/pssh.test --hosts=%s --user=%s --par=64 --outdir=%s --errdir=%s --timeout=60 /etc/hosts hosts < /dev/null" % (basedir, hostsFile.name, g_user, self.outDir, self.errDir)
+ rv = os.system(cmd)
+ self.assertEqual(rv, 0)
+
+ for host in g_hosts:
+ cmd = "ssh %s@%s cat /etc/hosts" % (g_user, host)
+ data = os.popen(cmd).read()
+ self.assertEqual(data, open("/tmp/pssh.test/%s/hosts" % host).read())
+
+ def testRecursive(self):
+ if os.path.exists("/tmp/pssh.test"):
+ try:
+ os.remove("/tmp/pssh.test")
+ except OSError:
+ shutil.rmtree("/tmp/pssh.test")
+
+ hostsFile = tempfile.NamedTemporaryFile()
+ hostsFile.write("".join(map(lambda x: "%s\n" % x, g_hosts)))
+ hostsFile.flush()
+ cmd = "%s/bin/pslurp -r -L /tmp/pssh.test -h %s -l %s -p 64 -o %s -e %s -t 60 /etc/init.d init.d < /dev/null" % (basedir, hostsFile.name, g_user, self.outDir, self.errDir)
+ rv = os.system(cmd)
+ self.assertEqual(rv, 0)
+
+ for host in g_hosts:
+ cmd = "ssh %s@%s ls -R /etc/init.d | sed 1d | sort" % (g_user, host)
+ data = os.popen(cmd).read()
+ self.assertEqual(data, os.popen("ls -R /tmp/pssh.test/%s/init.d | sed 1d | sort" % host).read())
+
+class PrsyncTest(unittest.TestCase):
+ def setUp(self):
+ self.outDir = tempfile.mkdtemp()
+ self.errDir = tempfile.mkdtemp()
+
+ def teardown(self):
+ shutil.rmtree(self.errDir)
+ shutil.rmtree(self.outDir)
+
+ def testShortOpts(self):
+ for host in g_hosts:
+ cmd = "ssh %s@%s rm -rf /tmp/pssh.test" % (g_user, host)
+ rv = os.system(cmd)
+ self.assertEqual(rv, 0)
+
+ hostsFile = tempfile.NamedTemporaryFile()
+ hostsFile.write("".join(map(lambda x: "%s\n" % x, g_hosts)))
+ hostsFile.flush()
+ cmd = "%s/bin/prsync -h %s -l %s -p 64 -o %s -e %s -t 60 -a -z /etc/hosts /tmp/pssh.test < /dev/null" % (basedir, hostsFile.name, g_user, self.outDir, self.errDir)
+ rv = os.system(cmd)
+ self.assertEqual(rv, 0)
+ for host in g_hosts:
+ cmd = "ssh %s@%s cat /tmp/pssh.test" % (g_user, host)
+ data = os.popen(cmd).read()
+ self.assertEqual(data, open("/etc/hosts").read())
+
+ def testLongOpts(self):
+ for host in g_hosts:
+ cmd = "ssh %s@%s rm -rf /tmp/pssh.test" % (g_user, host)
+ rv = os.system(cmd)
+ self.assertEqual(rv, 0)
+
+ hostsFile = tempfile.NamedTemporaryFile()
+ hostsFile.write("".join(map(lambda x: "%s\n" % x, g_hosts)))
+ hostsFile.flush()
+ cmd = "%s/bin/prsync --hosts=%s --user=%s --par=64 --outdir=%s --errdir=%s --timeout=60 --archive --compress /etc/hosts /tmp/pssh.test < /dev/null" % (basedir, hostsFile.name, g_user, self.outDir, self.errDir)
+ rv = os.system(cmd)
+ self.assertEqual(rv, 0)
+ for host in g_hosts:
+ cmd = "ssh %s@%s cat /tmp/pssh.test" % (g_user, host)
+ data = os.popen(cmd).read()
+ self.assertEqual(data, open("/etc/hosts").read())
+
+ def testRecursive(self):
+ for host in g_hosts:
+ cmd = "ssh %s@%s rm -rf /tmp/pssh.test" % (g_user, host)
+ rv = os.system(cmd)
+ self.assertEqual(rv, 0)
+
+ hostsFile = tempfile.NamedTemporaryFile()
+ hostsFile.write("".join(map(lambda x: "%s\n" % x, g_hosts)))
+ hostsFile.flush()
+ cmd = "%s/bin/prsync -r -h %s -l %s -p 64 -o %s -e %s -t 60 -a -z /etc/init.d/ /tmp/pssh.test < /dev/null" % (basedir, hostsFile.name, g_user, self.outDir, self.errDir)
+ rv = os.system(cmd)
+ self.assertEqual(rv, 0)
+ files = os.popen("ls -R /etc/init.d | sed 1d | sort").read().strip()
+ for host in g_hosts:
+ cmd = "ssh %s@%s ls -R /tmp/pssh.test | sed 1d | sort" % (g_user, host)
+ data = os.popen(cmd).read().strip()
+ self.assertEqual(data, files)
+
+class PnukeTest(unittest.TestCase):
+ def setUp(self):
+ self.outDir = tempfile.mkdtemp()
+ self.errDir = tempfile.mkdtemp()
+
+ def teardown(self):
+ shutil.rmtree(self.errDir)
+ shutil.rmtree(self.outDir)
+
+ def testShortOpts(self):
+ hostsFile = tempfile.NamedTemporaryFile()
+ hostsFile.write("".join(map(lambda x: "%s\n" % x, g_hosts)))
+ hostsFile.flush()
+ cmd = "%s/bin/pssh -h %s -l %s -p 64 -o %s -e %s -t 60 -v sleep 60 < /dev/null &" % (basedir, hostsFile.name, g_user, self.outDir, self.errDir)
+ os.system(cmd)
+ time.sleep(5)
+
+ cmd = "%s/bin/pnuke -h %s -l %s -p 64 -o %s -e %s -t 60 -v sleep < /dev/null" % (basedir, hostsFile.name, g_user, self.outDir, self.errDir)
+ print cmd
+ rv = os.system(cmd)
+ self.assertEqual(rv, 0)
+
+ def testLongOpts(self):
+ hostsFile = tempfile.NamedTemporaryFile()
+ hostsFile.write("".join(map(lambda x: "%s\n" % x, g_hosts)))
+ hostsFile.flush()
+ cmd = "%s/bin/pssh --hosts=%s --user=%s --par=64 --outdir=%s --errdir=%s --timeout=60 --verbose sleep 60 < /dev/null &" % (basedir, hostsFile.name, g_user, self.outDir, self.errDir)
+ os.system(cmd)
+ time.sleep(5)
+
+ cmd = "%s/bin/pnuke --hosts=%s --user=%s --par=64 --outdir=%s --errdir=%s --timeout=60 --verbose sleep < /dev/null" % (basedir, hostsFile.name, g_user, self.outDir, self.errDir)
+ print cmd
+ rv = os.system(cmd)
+ self.assertEqual(rv, 0)
+
+if __name__ == '__main__':
+ suite = unittest.TestSuite()
+ suite.addTest(unittest.makeSuite(PsshTest, "test"))
+ suite.addTest(unittest.makeSuite(PscpTest, "test"))
+ suite.addTest(unittest.makeSuite(PslurpTest, "test"))
+ suite.addTest(unittest.makeSuite(PrsyncTest, "test"))
+ suite.addTest(unittest.makeSuite(PnukeTest, "test"))
+ unittest.TextTestRunner().run(suite)