diff options
Diffstat (limited to 'planetlab')
| -rw-r--r-- | planetlab/host.txt | 396 | ||||
| -rw-r--r-- | planetlab/pssh/AUTHORS | 2 | ||||
| -rw-r--r-- | planetlab/pssh/COPYING | 32 | ||||
| -rw-r--r-- | planetlab/pssh/ChangeLog | 190 | ||||
| -rw-r--r-- | planetlab/pssh/INSTALL | 25 | ||||
| -rw-r--r-- | planetlab/pssh/PKG-INFO | 28 | ||||
| -rwxr-xr-x | planetlab/pssh/bin/pnuke | 93 | ||||
| -rwxr-xr-x | planetlab/pssh/bin/prsync | 125 | ||||
| -rwxr-xr-x | planetlab/pssh/bin/pscp | 108 | ||||
| -rwxr-xr-x | planetlab/pssh/bin/pslurp | 129 | ||||
| -rwxr-xr-x | planetlab/pssh/bin/pssh | 115 | ||||
| -rwxr-xr-x | planetlab/pssh/bin/pssh-askpass | 11 | ||||
| -rw-r--r-- | planetlab/pssh/man/man1/pssh.1 | 330 | ||||
| -rw-r--r-- | planetlab/pssh/psshlib/__init__.py | 0 | ||||
| -rw-r--r-- | planetlab/pssh/psshlib/askpass_client.py | 95 | ||||
| -rw-r--r-- | planetlab/pssh/psshlib/askpass_server.py | 101 | ||||
| -rw-r--r-- | planetlab/pssh/psshlib/cli.py | 108 | ||||
| -rw-r--r-- | planetlab/pssh/psshlib/color.py | 39 | ||||
| -rw-r--r-- | planetlab/pssh/psshlib/manager.py | 345 | ||||
| -rw-r--r-- | planetlab/pssh/psshlib/psshutil.py | 108 | ||||
| -rw-r--r-- | planetlab/pssh/psshlib/task.py | 281 | ||||
| -rw-r--r-- | planetlab/pssh/setup.py | 41 | ||||
| -rw-r--r-- | planetlab/pssh/test/test.py | 302 |
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) |
