3f28bdbf2bef528b4d112ae427a6417135685f44
[deployable.git] / deploy
blob3f28bdbf2bef528b4d112ae427a6417135685f44
1 #!/usr/bin/env perl
2 use strict;
3 use warnings;
4 my $VERSION = '0.6.0';
5 use Carp;
6 use Pod::Usage qw( pod2usage );
7 use Getopt::Long qw( :config gnu_getopt );
8 use English qw( -no_match_vars );
9 use Net::SSH::Perl;
10 use Net::SSH::Perl::Auth;
11 use Net::SFTP;
12 use Net::SFTP::Attributes;
13 use IO::Prompt;
14 use Data::Dumper;
15 use File::Spec::Functions qw( catfile );
17 my %config = (
18 username => 'root',
19 debug => 0,
20 dir => '/tmp/our-deploy',
21 prompt => 1,
23 GetOptions(
24 \%config,
25 qw(
26 usage! help! man! version!
28 compress|c!
29 debug|D!
30 dir|directory|d=s
31 password|pass|p:s
32 prompt|P!
33 script|s=s
34 stderr|E!
35 stdout|O!
36 username|user|u=s
39 pod2usage(message => "$0 $VERSION", -verbose => 99, -sections => ' ')
40 if $config{version};
41 pod2usage(-verbose => 99, -sections => 'USAGE') if $config{usage};
42 pod2usage(-verbose => 99, -sections => 'USAGE|EXAMPLES|OPTIONS')
43 if $config{help};
44 pod2usage(-verbose => 2) if $config{man};
46 pod2usage(-verbose => 99, -sections => 'USAGE',
47 message => 'Only one allowed between --stdout and --stderr')
48 if $config{stdout} && $config{stderr};
50 # Script implementation here
51 my @hostnames = @ARGV;
52 @ARGV = ();
54 $config{password} = prompt 'password: ', -e => '*'
55 unless exists $config{password};
57 ($config{remote} = $config{script}) =~ s{[^\w.-]}{}mxsg;
58 $config{remote} = catfile($config{dir}, $config{remote});
60 for my $hostname (@hostnames) {
61 eval { operate_on_host($hostname) };
62 carp $EVAL_ERROR if $EVAL_ERROR;
65 sub operate_on_host {
66 my ($hostname) = @_;
67 my $remote = $config{remote};
69 print {*STDOUT} "*** OPERATING ON $hostname ***\n";
70 if ($config{prompt}) {
71 my $choice = lc(prompt "$hostname - continue? (Yes | Skip | No) ",
72 -while => qr/\A[nsy]\z/mxs);
73 return if $choice eq 's';
74 exit 0 if $choice eq 'n';
75 } ## end if ($config{prompt})
77 # Transfer file into $remote
78 my $sftp = get_sftp($hostname);
79 make_path($sftp, $config{dir});
80 $sftp->put($config{script}, $remote);
81 croak "no $remote, sorry. Stopped" unless $sftp->do_stat($remote);
83 # Execute file
84 my $ssh = get_ssh($hostname);
85 $|++;
86 print {*STDOUT} "$remote ";
87 my ($out, $err, $exit) = $ssh->cmd($remote);
88 print {*STDOUT} "exit = $exit\n";
90 if ($config{stdout} && defined $out) {
91 print {*STDOUT} $out;
93 elsif ($config{stderr} && defined $err) {
94 print {*STDOUT} $err;
96 else {
97 for ([STDOUT => $out], [STDERR => $err]) {
98 my ($type, $val) = @$_;
99 next unless defined $val;
100 $val =~ s{\s+\z}{}mxs;
101 $val =~ s{^}{$type }gmxs;
102 print {*STDOUT} $val, "\n\n";
103 } ## end for ([STDOUT => $out], ...
106 return;
107 } ## end sub operate_on_host
109 sub make_path {
110 my ($sftp, $fullpath) = @_;
112 my $path = '';
113 for my $chunk (split m{/}mxs, $fullpath) {
114 $path .= $chunk . '/'; # works fine with the root
115 next if $sftp->do_stat($path);
116 $sftp->do_mkdir($path, Net::SFTP::Attributes->new());
118 croak "no $path, sorry. Stopped" unless $sftp->do_stat($path);
120 return;
121 } ## end sub make_path
123 sub get_ssh {
124 my ($hostname) = @_;
125 my $ssh = Net::SSH::Perl->new(
126 $hostname,
127 protocol => 2,
128 debug => $config{debug},
130 $ssh->login($config{username}, $config{password}, 'suppress_shell');
132 return $ssh;
133 } ## end sub get_ssh
135 sub get_sftp {
136 my ($hostname) = @_;
137 return Net::SFTP->new(
138 $hostname,
139 warn => sub { },
140 user => $config{username},
141 password => $config{password},
142 ssh_args => {
143 protocol => 2,
144 debug => $config{debug},
145 compression => $config{compress},
148 } ## end sub get_sftp
150 __END__
152 =head1 NAME
154 deploy - deploy a script on one or more remote hosts, via ssh
156 =head1 VERSION
158 See version at beginning of script, variable $VERSION, or call
160 shell$ deploy --version
162 =head1 USAGE
164 deploy [--usage] [--help] [--man] [--version]
166 deploy [--debug|-D] [--dir|--directory|-d <dirname>]
167 [--password|--pass|-p] [--prompt|-P]
168 [--script|-s <scriptname>] [--stderr|-E] [--stdout|-O]
169 [--username|--user|-u]
171 =head1 EXAMPLES
173 shell$ deploy
175 # Upload deploy-script.pl and execute it on each server listed
176 # in file "targets"
177 shell$ deploy -s deploy-script.pl `cat targets`
179 # ... without bugging me prompting confirmations...
180 shell$ deploy -s deploy-script.pl --no-prompt `cat targets`
182 =head1 DESCRIPTION
184 This utility allows you to I<deploy> a script to one or more remote
185 hosts. Thus, you can provide a script that will be uploaded (via
186 B<sftp>) to the remote host and executed (via B<ssh>).
188 Before operations start for each host you will be prompted for
189 continuation: you can choose to go, skip or quit. You can disable
190 this by specifying C<--no-prompt>.
192 By default, directory C</tmp/our-deploy> on the target system will be
193 used. You can provide your own working directory path on the target system
194 via the C<--dir|--directory|-d> option. The directory will be created
195 if it does not exist.
197 For logging in, you can provide your own username/password pair directly
198 on the command line. Note that this utility explicitly avoids public
199 key authentication in favour of username/password authentication. Don't
200 ask me why, this may change in the future. Anyway, you're not obliged
201 to provide either on the command line: the username defaults to C<root>,
202 and you'll be prompted to provide a password if you don't put any
203 on the command line. The prompt does not show the password on the terminal.
205 By default, L<Net::SSH::Perl> will try to use public/private key
206 authentication. If you're confident that this method will work, you can
207 just hit enter when requested for a password, or you can pass
208 C<-p> without a password on the command line (you can actually pass
209 every password you can think of, it will be ignored).
211 =head2 Output Format
213 The normal output format is geared at easing parsing by other programs. It
214 is compound of the following parts:
216 =over
218 =item *
220 a single line specifing the hostname/ip, with the following format:
222 *** OPERATING ON <hostname> ***
224 =item *
226 a single line reporting the exit code from the remote process, with the
227 following format:
229 </path/to/deployed/program> exit = <exit-code>
231 =item *
233 0 or more lines starting with C<STDOUT > (note the space);
235 =item *
237 0 or more lines starting with C<STDERR > (note the space).
239 =back
241 If any of L</--stderr> or L</--stdout> are present, then the relevant
242 channel is printed on STDOUT immediately after the first two lines of the
243 format above, unchanged.
245 =head2 Example Runs
247 Suppose to have the following script F<bar.sh> to deploy:
249 #!/bin/bash
251 echo 'Hi there!'
252 ls baz
253 echo 'How are you all?!?'
255 If you don't provide any of L</--stderr> or L</--stdout>, you will have
256 something like this:
258 *** OPERATING ON foo.example.com ***
259 /tmp/our-deploy/bar.sh exit = 0
260 STDOUT Hi there!
261 STDOUT How are you all?!?
262 STDERR ls: baz: No such file or directory
264 If you pass L<--stderr> you will get:
266 *** OPERATING ON foo.example.com ***
267 /tmp/our-deploy/bar.sh exit = 0
268 ls: baz: No such file or directory
270 If you pass L<--stdout> you will get:
272 *** OPERATING ON foo.example.com ***
273 /tmp/our-deploy/bar.sh exit = 0
274 Hi there!
275 How are you all?!?
277 =head1 OPTIONS
279 =over
281 =item --debug | -D
283 turns on debug mode, which should print out more stuff during operations.
284 You should not need it as a user.
286 =item --dir | --directory | -d <dirname>
288 specify the working directory on the target system. This is the
289 directory into which the deploy script will be uploaded. It will
290 be created if it does not exist.
292 Defaults to C</tmp/our-deploy>.
294 =item --help
296 print a somewhat more verbose help, showing usage, this description of
297 the options and some examples from the synopsis.
299 =item --man
301 print out the full documentation for the script.
303 =item --password | --pass | -p <password>
305 you can specify the password on the command line, even if it's probably
306 best B<NOT> to do so and wait for the program to prompt you one.
308 By default, you'll be prompted a password and this will not be written
309 on the terminal.
311 =item --prompt | -P
313 this option enables prompting before operations are started on each
314 host. When the prompt is enabled, you're presented with three choices:
316 =over
318 =item -
320 B<Yes> continue deployment on the given host;
322 =item -
324 B<Skip> skip this host;
326 =item -
328 B<No> stop deployment and exit.
330 =back
332 One letter suffices. By default, C<Yes> is assumed.
334 By default this option is I<always> active, so you're probably
335 interested in C<--no-prompt> to disable it.
337 =item --script | -s <scriptname>
339 set the script/program to upload and execute. This script will be uploaded
340 to the target system (see C<--directory|-d> above), but the name of the
341 script will be sanitised (only alphanumeric, C<_>, C<.> and C<-> will
342 be retained), so be careful if you have to look for the uploaded
343 script later.
345 =item --stderr | -E
347 select only the STDERR channel from the responses got via SSH. This
348 option cannot be used with L</--stdout>.
350 =item --stdout | -O
352 select only the STDOUT channel from the responses got via SSH. This
353 option cannot be used with L</--stderr>.
355 =item --usage
357 print a concise usage line and exit.
359 =item --username | --user | -u <username>
361 specify the user name to use for logging into the remote host(s).
363 Defaults to C<root>.
365 =item --version
367 print the version of the script.
369 =back
371 =head1 DIAGNOSTICS
373 =over
375 =item C<< no %s, sorry. Stopped at... >>
377 The given element is not available on the target system.
379 In case of the directory, this means that the automatic creation
380 process did not work for any reason. In case of the script, this
381 means that the file upload did not work.
383 =back
386 =head1 CONFIGURATION AND ENVIRONMENT
388 deploy requires no configuration files or environment variables.
391 =head1 DEPENDENCIES
393 =over
395 =item -
397 L<IO::Prompt>
399 =item -
401 L<Net::SFTP>
403 =item -
405 L<Net::SSH::Perl>
407 =back
410 =head1 BUGS AND LIMITATIONS
412 No bugs have been reported.
414 Please report any bugs or feature requests through http://rt.cpan.org/
417 =head1 AUTHOR
419 Flavio Poletti C<flavio@polettix.it>
422 =head1 LICENCE AND COPYRIGHT
424 Copyright (c) 2007-2008, Flavio Poletti C<flavio@polettix.it>.
425 All rights reserved.
427 This script is free software; you can redistribute it and/or
428 modify it under the same terms as Perl itself. See L<perlartistic>
429 and L<perlgpl>.
431 Questo script è software libero: potete ridistribuirlo e/o
432 modificarlo negli stessi termini di Perl stesso. Vedete anche
433 L<perlartistic> e L<perlgpl>.
436 =head1 DISCLAIMER OF WARRANTY
438 BECAUSE THIS SOFTWARE IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
439 FOR THE SOFTWARE, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
440 OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
441 PROVIDE THE SOFTWARE "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER
442 EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
443 WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE
444 ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE SOFTWARE IS WITH
445 YOU. SHOULD THE SOFTWARE PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL
446 NECESSARY SERVICING, REPAIR, OR CORRECTION.
448 IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
449 WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
450 REDISTRIBUTE THE SOFTWARE AS PERMITTED BY THE ABOVE LICENCE, BE
451 LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL,
452 OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE
453 THE SOFTWARE (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING
454 RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A
455 FAILURE OF THE SOFTWARE TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF
456 SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
457 SUCH DAMAGES.
459 =head1 NEGAZIONE DELLA GARANZIA
461 Poiché questo software viene dato con una licenza gratuita, non
462 c'è alcuna garanzia associata ad esso, ai fini e per quanto permesso
463 dalle leggi applicabili. A meno di quanto possa essere specificato
464 altrove, il proprietario e detentore del copyright fornisce questo
465 software "così com'è" senza garanzia di alcun tipo, sia essa espressa
466 o implicita, includendo fra l'altro (senza però limitarsi a questo)
467 eventuali garanzie implicite di commerciabilità e adeguatezza per
468 uno scopo particolare. L'intero rischio riguardo alla qualità ed
469 alle prestazioni di questo software rimane a voi. Se il software
470 dovesse dimostrarsi difettoso, vi assumete tutte le responsabilità
471 ed i costi per tutti i necessari servizi, riparazioni o correzioni.
473 In nessun caso, a meno che ciò non sia richiesto dalle leggi vigenti
474 o sia regolato da un accordo scritto, alcuno dei detentori del diritto
475 di copyright, o qualunque altra parte che possa modificare, o redistribuire
476 questo software così come consentito dalla licenza di cui sopra, potrà
477 essere considerato responsabile nei vostri confronti per danni, ivi
478 inclusi danni generali, speciali, incidentali o conseguenziali, derivanti
479 dall'utilizzo o dall'incapacità di utilizzo di questo software. Ciò
480 include, a puro titolo di esempio e senza limitarsi ad essi, la perdita
481 di dati, l'alterazione involontaria o indesiderata di dati, le perdite
482 sostenute da voi o da terze parti o un fallimento del software ad
483 operare con un qualsivoglia altro software. Tale negazione di garanzia
484 rimane in essere anche se i dententori del copyright, o qualsiasi altra
485 parte, è stata avvisata della possibilità di tali danneggiamenti.
487 Se decidete di utilizzare questo software, lo fate a vostro rischio
488 e pericolo. Se pensate che i termini di questa negazione di garanzia
489 non si confacciano alle vostre esigenze, o al vostro modo di
490 considerare un software, o ancora al modo in cui avete sempre trattato
491 software di terze parti, non usatelo. Se lo usate, accettate espressamente
492 questa negazione di garanzia e la piena responsabilità per qualsiasi
493 tipo di danno, di qualsiasi natura, possa derivarne.
495 =cut