Slight fixes for password handling.
[deployable.git] / deploy
blob7fd3a2d9b93d004eafee551f83732324bf58e614
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 if (exists $config{password}) {
55 $config{interactive} = 1;
56 $config{identity_files} = [];
57 $config{password} = prompt 'password: ', -e => '*'
58 unless $config{password};
61 ($config{remote} = $config{script}) =~ s{[^\w.-]}{}mxsg;
62 $config{remote} = catfile($config{dir}, $config{remote});
64 for my $hostname (@hostnames) {
65 eval { operate_on_host($hostname) };
66 carp $EVAL_ERROR if $EVAL_ERROR;
69 sub operate_on_host {
70 my ($hostname) = @_;
71 my $remote = $config{remote};
73 print {*STDOUT} "*** OPERATING ON $hostname ***\n";
74 if ($config{prompt}) {
75 my $choice = lc(prompt "$hostname - continue? (Yes | Skip | No) ",
76 -while => qr/\A[nsy]\z/mxs);
77 return if $choice eq 's';
78 exit 0 if $choice eq 'n';
79 } ## end if ($config{prompt})
81 # Transfer file into $remote
82 my $sftp = get_sftp($hostname);
83 make_path($sftp, $config{dir});
84 $sftp->put($config{script}, $remote);
85 croak "no $remote, sorry. Stopped" unless $sftp->do_stat($remote);
87 # Execute file
88 my $ssh = get_ssh($hostname);
89 $|++;
90 print {*STDOUT} "$remote ";
91 my ($out, $err, $exit) = $ssh->cmd($remote);
92 print {*STDOUT} "exit = $exit\n";
94 if ($config{stdout} && defined $out) {
95 print {*STDOUT} $out;
97 elsif ($config{stderr} && defined $err) {
98 print {*STDOUT} $err;
100 else {
101 for ([STDOUT => $out], [STDERR => $err]) {
102 my ($type, $val) = @$_;
103 next unless defined $val;
104 $val =~ s{\s+\z}{}mxs;
105 $val =~ s{^}{$type }gmxs;
106 print {*STDOUT} $val, "\n\n";
107 } ## end for ([STDOUT => $out], ...
110 return;
111 } ## end sub operate_on_host
113 sub make_path {
114 my ($sftp, $fullpath) = @_;
116 my $path = '';
117 for my $chunk (split m{/}mxs, $fullpath) {
118 $path .= $chunk . '/'; # works fine with the root
119 next if $sftp->do_stat($path);
120 $sftp->do_mkdir($path, Net::SFTP::Attributes->new());
122 croak "no $path, sorry. Stopped" unless $sftp->do_stat($path);
124 return;
125 } ## end sub make_path
127 sub _get_optionals {
128 map { $_ => $config{$_} } grep { exists $config{$_} } qw( interactive identity_files password );
131 sub get_ssh {
132 my ($hostname) = @_;
133 my %optional;
135 my $ssh = Net::SSH::Perl->new(
136 $hostname,
137 protocol => 2,
138 debug => $config{debug},
139 _get_optionals(),
141 $ssh->login($config{username}, $config{password}, 'suppress_shell');
143 return $ssh;
144 } ## end sub get_ssh
146 sub get_sftp {
147 my ($hostname) = @_;
148 return Net::SFTP->new(
149 $hostname,
150 warn => sub { },
151 user => $config{username},
152 password => $config{password},
153 ssh_args => {
154 protocol => 2,
155 debug => $config{debug},
156 compression => $config{compress},
157 user => $config{username},
158 _get_optionals(),
161 } ## end sub get_sftp
163 __END__
165 =head1 NAME
167 deploy - deploy a script on one or more remote hosts, via ssh
169 =head1 VERSION
171 See version at beginning of script, variable $VERSION, or call
173 shell$ deploy --version
175 =head1 USAGE
177 deploy [--usage] [--help] [--man] [--version]
179 deploy [--debug|-D] [--dir|--directory|-d <dirname>]
180 [--password|--pass|-p] [--prompt|-P]
181 [--script|-s <scriptname>] [--stderr|-E] [--stdout|-O]
182 [--username|--user|-u]
184 =head1 EXAMPLES
186 shell$ deploy
188 # Upload deploy-script.pl and execute it on each server listed
189 # in file "targets"
190 shell$ deploy -s deploy-script.pl `cat targets`
192 # ... without bugging me prompting confirmations...
193 shell$ deploy -s deploy-script.pl --no-prompt `cat targets`
195 =head1 DESCRIPTION
197 This utility allows you to I<deploy> a script to one or more remote
198 hosts. Thus, you can provide a script that will be uploaded (via
199 B<sftp>) to the remote host and executed (via B<ssh>).
201 Before operations start for each host you will be prompted for
202 continuation: you can choose to go, skip or quit. You can disable
203 this by specifying C<--no-prompt>.
205 By default, directory C</tmp/our-deploy> on the target system will be
206 used. You can provide your own working directory path on the target system
207 via the C<--dir|--directory|-d> option. The directory will be created
208 if it does not exist.
210 For logging in, you can provide your own username/password pair directly
211 on the command line. Note that this utility explicitly avoids public
212 key authentication in favour of username/password authentication. Don't
213 ask me why, this may change in the future. Anyway, you're not obliged
214 to provide either on the command line: the username defaults to C<root>,
215 and you'll be prompted to provide a password if you don't put any
216 on the command line but specify the C<--password|-p> option without a value.
217 The prompt does not show the password on the terminal.
219 By default, L<Net::SSH::Perl> will try to use public/private key
220 authentication. If you're confident that this method will work, you can
221 just hit enter when requested for a password, or you can pass
222 C<-p> without a password on the command line (you can actually pass
223 every password you can think of, it will be ignored).
225 =head2 Output Format
227 The normal output format is geared at easing parsing by other programs. It
228 is compound of the following parts:
230 =over
232 =item *
234 a single line specifing the hostname/ip, with the following format:
236 *** OPERATING ON <hostname> ***
238 =item *
240 a single line reporting the exit code from the remote process, with the
241 following format:
243 </path/to/deployed/program> exit = <exit-code>
245 =item *
247 0 or more lines starting with C<STDOUT > (note the space);
249 =item *
251 0 or more lines starting with C<STDERR > (note the space).
253 =back
255 If any of L</--stderr> or L</--stdout> are present, then the relevant
256 channel is printed on STDOUT immediately after the first two lines of the
257 format above, unchanged.
259 =head2 Example Runs
261 Suppose to have the following script F<bar.sh> to deploy:
263 #!/bin/bash
265 echo 'Hi there!'
266 ls baz
267 echo 'How are you all?!?'
269 If you don't provide any of L</--stderr> or L</--stdout>, you will have
270 something like this:
272 *** OPERATING ON foo.example.com ***
273 /tmp/our-deploy/bar.sh exit = 0
274 STDOUT Hi there!
275 STDOUT How are you all?!?
276 STDERR ls: baz: No such file or directory
278 If you pass L<--stderr> you will get:
280 *** OPERATING ON foo.example.com ***
281 /tmp/our-deploy/bar.sh exit = 0
282 ls: baz: No such file or directory
284 If you pass L<--stdout> you will get:
286 *** OPERATING ON foo.example.com ***
287 /tmp/our-deploy/bar.sh exit = 0
288 Hi there!
289 How are you all?!?
291 =head1 OPTIONS
293 =over
295 =item --debug | -D
297 turns on debug mode, which should print out more stuff during operations.
298 You should not need it as a user.
300 =item --dir | --directory | -d <dirname>
302 specify the working directory on the target system. This is the
303 directory into which the deploy script will be uploaded. It will
304 be created if it does not exist.
306 Defaults to C</tmp/our-deploy>.
308 =item --help
310 print a somewhat more verbose help, showing usage, this description of
311 the options and some examples from the synopsis.
313 =item --man
315 print out the full documentation for the script.
317 =item --password | --pass | -p <password>
319 you can specify the password on the command line, even if it's probably
320 best B<NOT> to do so and wait for the program to prompt you one.
322 By default, you'll be prompted a password and this will not be written
323 on the terminal.
325 =item --prompt | -P
327 this option enables prompting before operations are started on each
328 host. When the prompt is enabled, you're presented with three choices:
330 =over
332 =item -
334 B<Yes> continue deployment on the given host;
336 =item -
338 B<Skip> skip this host;
340 =item -
342 B<No> stop deployment and exit.
344 =back
346 One letter suffices. By default, C<Yes> is assumed.
348 By default this option is I<always> active, so you're probably
349 interested in C<--no-prompt> to disable it.
351 =item --script | -s <scriptname>
353 set the script/program to upload and execute. This script will be uploaded
354 to the target system (see C<--directory|-d> above), but the name of the
355 script will be sanitised (only alphanumeric, C<_>, C<.> and C<-> will
356 be retained), so be careful if you have to look for the uploaded
357 script later.
359 =item --stderr | -E
361 select only the STDERR channel from the responses got via SSH. This
362 option cannot be used with L</--stdout>.
364 =item --stdout | -O
366 select only the STDOUT channel from the responses got via SSH. This
367 option cannot be used with L</--stderr>.
369 =item --usage
371 print a concise usage line and exit.
373 =item --username | --user | -u <username>
375 specify the user name to use for logging into the remote host(s).
377 Defaults to C<root>.
379 =item --version
381 print the version of the script.
383 =back
385 =head1 DIAGNOSTICS
387 =over
389 =item C<< no %s, sorry. Stopped at... >>
391 The given element is not available on the target system.
393 In case of the directory, this means that the automatic creation
394 process did not work for any reason. In case of the script, this
395 means that the file upload did not work.
397 =back
400 =head1 CONFIGURATION AND ENVIRONMENT
402 deploy requires no configuration files or environment variables.
405 =head1 DEPENDENCIES
407 =over
409 =item -
411 L<IO::Prompt>
413 =item -
415 L<Net::SFTP>
417 =item -
419 L<Net::SSH::Perl>
421 =back
424 =head1 BUGS AND LIMITATIONS
426 No bugs have been reported.
428 Please report any bugs or feature requests through http://rt.cpan.org/
431 =head1 AUTHOR
433 Flavio Poletti C<flavio@polettix.it>
436 =head1 LICENCE AND COPYRIGHT
438 Copyright (c) 2007-2008, Flavio Poletti C<flavio@polettix.it>.
439 All rights reserved.
441 This script is free software; you can redistribute it and/or
442 modify it under the same terms as Perl itself. See L<perlartistic>
443 and L<perlgpl>.
445 Questo script è software libero: potete ridistribuirlo e/o
446 modificarlo negli stessi termini di Perl stesso. Vedete anche
447 L<perlartistic> e L<perlgpl>.
450 =head1 DISCLAIMER OF WARRANTY
452 BECAUSE THIS SOFTWARE IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
453 FOR THE SOFTWARE, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
454 OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
455 PROVIDE THE SOFTWARE "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER
456 EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
457 WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE
458 ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE SOFTWARE IS WITH
459 YOU. SHOULD THE SOFTWARE PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL
460 NECESSARY SERVICING, REPAIR, OR CORRECTION.
462 IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
463 WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
464 REDISTRIBUTE THE SOFTWARE AS PERMITTED BY THE ABOVE LICENCE, BE
465 LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL,
466 OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE
467 THE SOFTWARE (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING
468 RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A
469 FAILURE OF THE SOFTWARE TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF
470 SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
471 SUCH DAMAGES.
473 =head1 NEGAZIONE DELLA GARANZIA
475 Poiché questo software viene dato con una licenza gratuita, non
476 c'è alcuna garanzia associata ad esso, ai fini e per quanto permesso
477 dalle leggi applicabili. A meno di quanto possa essere specificato
478 altrove, il proprietario e detentore del copyright fornisce questo
479 software "così com'è" senza garanzia di alcun tipo, sia essa espressa
480 o implicita, includendo fra l'altro (senza però limitarsi a questo)
481 eventuali garanzie implicite di commerciabilità e adeguatezza per
482 uno scopo particolare. L'intero rischio riguardo alla qualità ed
483 alle prestazioni di questo software rimane a voi. Se il software
484 dovesse dimostrarsi difettoso, vi assumete tutte le responsabilità
485 ed i costi per tutti i necessari servizi, riparazioni o correzioni.
487 In nessun caso, a meno che ciò non sia richiesto dalle leggi vigenti
488 o sia regolato da un accordo scritto, alcuno dei detentori del diritto
489 di copyright, o qualunque altra parte che possa modificare, o redistribuire
490 questo software così come consentito dalla licenza di cui sopra, potrà
491 essere considerato responsabile nei vostri confronti per danni, ivi
492 inclusi danni generali, speciali, incidentali o conseguenziali, derivanti
493 dall'utilizzo o dall'incapacità di utilizzo di questo software. Ciò
494 include, a puro titolo di esempio e senza limitarsi ad essi, la perdita
495 di dati, l'alterazione involontaria o indesiderata di dati, le perdite
496 sostenute da voi o da terze parti o un fallimento del software ad
497 operare con un qualsivoglia altro software. Tale negazione di garanzia
498 rimane in essere anche se i dententori del copyright, o qualsiasi altra
499 parte, è stata avvisata della possibilità di tali danneggiamenti.
501 Se decidete di utilizzare questo software, lo fate a vostro rischio
502 e pericolo. Se pensate che i termini di questa negazione di garanzia
503 non si confacciano alle vostre esigenze, o al vostro modo di
504 considerare un software, o ancora al modo in cui avete sempre trattato
505 software di terze parti, non usatelo. Se lo usate, accettate espressamente
506 questa negazione di garanzia e la piena responsabilità per qualsiasi
507 tipo di danno, di qualsiasi natura, possa derivarne.
509 =cut