Added one-shot command-line remote execution
[deployable.git] / deploy
blob385c8ba4673e4763a2e67f70c1891d992f2fe523
1 #!/usr/bin/env perl
2 use strict;
3 use warnings;
4 my $VERSION = '0.7.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 commandline|command-line|S=s
35 stderr|E!
36 stdout|O!
37 username|user|u=s
40 pod2usage(message => "$0 $VERSION", -verbose => 99, -sections => ' ')
41 if $config{version};
42 pod2usage(-verbose => 99, -sections => 'USAGE') if $config{usage};
43 pod2usage(-verbose => 99, -sections => 'USAGE|EXAMPLES|OPTIONS')
44 if $config{help};
45 pod2usage(-verbose => 2) if $config{man};
47 pod2usage(-verbose => 99, -sections => 'USAGE',
48 message => 'Only one allowed between --stdout and --stderr')
49 if $config{stdout} && $config{stderr};
51 # Script implementation here
52 my @hostnames = @ARGV;
53 @ARGV = ();
55 if (exists $config{password}) {
56 $config{interactive} = 1;
57 $config{identity_files} = [];
58 $config{password} = prompt 'password: ', -e => '*'
59 unless $config{password};
62 if ($config{commandline}) {
63 pod2usage(-verbose => 99, -sections => 'USAGE',
64 message => 'use only one of "script" and "command-line"')
65 if exists $config{script};
66 $config{remote} = $config{commandline};
68 else {
69 ($config{remote} = $config{script}) =~ s{[^\w.-]}{}mxsg;
70 $config{remote} = catfile($config{dir}, $config{remote});
73 for my $hostname (@hostnames) {
74 eval { operate_on_host($hostname) };
75 carp $EVAL_ERROR if $EVAL_ERROR;
78 sub operate_on_host {
79 my ($hostname) = @_;
80 my $remote = $config{remote};
82 print {*STDOUT} "*** OPERATING ON $hostname ***\n";
83 if ($config{prompt}) {
84 my $choice = lc(prompt "$hostname - continue? (Yes | Skip | No) ",
85 -while => qr/\A[nsy]\z/mxs);
86 return if $choice eq 's';
87 exit 0 if $choice eq 'n';
88 } ## end if ($config{prompt})
90 # Transfer file into $remote, if any
91 if ($config{script}) {
92 my $sftp = get_sftp($hostname);
93 make_path($sftp, $config{dir});
94 $sftp->put($config{script}, $remote);
95 croak "no $remote, sorry. Stopped" unless $sftp->do_stat($remote);
98 # Execute
99 my $ssh = get_ssh($hostname);
100 $|++;
101 print {*STDOUT} $config{script} ? "$remote " : "cmd[$remote] ";
102 my ($out, $err, $exit) = $ssh->cmd($remote);
103 print {*STDOUT} "exit = $exit\n";
105 if ($config{stdout} && defined $out) {
106 print {*STDOUT} $out;
108 elsif ($config{stderr} && defined $err) {
109 print {*STDOUT} $err;
111 else {
112 for ([STDOUT => $out], [STDERR => $err]) {
113 my ($type, $val) = @$_;
114 next unless defined $val;
115 $val =~ s{\s+\z}{}mxs;
116 $val =~ s{^}{$type }gmxs;
117 print {*STDOUT} $val, "\n\n";
118 } ## end for ([STDOUT => $out], ...
121 return;
122 } ## end sub operate_on_host
124 sub make_path {
125 my ($sftp, $fullpath) = @_;
127 my $path = '';
128 for my $chunk (split m{/}mxs, $fullpath) {
129 $path .= $chunk . '/'; # works fine with the root
130 next if $sftp->do_stat($path);
131 $sftp->do_mkdir($path, Net::SFTP::Attributes->new());
133 croak "no $path, sorry. Stopped" unless $sftp->do_stat($path);
135 return;
136 } ## end sub make_path
138 sub _get_optionals {
139 map { $_ => $config{$_} } grep { exists $config{$_} } qw( interactive identity_files password );
142 sub get_ssh {
143 my ($hostname) = @_;
144 my %optional;
146 my $ssh = Net::SSH::Perl->new(
147 $hostname,
148 protocol => 2,
149 debug => $config{debug},
150 _get_optionals(),
152 $ssh->login($config{username}, $config{password}, 'suppress_shell');
154 return $ssh;
155 } ## end sub get_ssh
157 sub get_sftp {
158 my ($hostname) = @_;
159 return Net::SFTP->new(
160 $hostname,
161 warn => sub { },
162 user => $config{username},
163 password => $config{password},
164 ssh_args => {
165 protocol => 2,
166 debug => $config{debug},
167 compression => $config{compress},
168 user => $config{username},
169 _get_optionals(),
172 } ## end sub get_sftp
174 __END__
176 =head1 NAME
178 deploy - deploy a script on one or more remote hosts, via ssh
180 =head1 VERSION
182 See version at beginning of script, variable $VERSION, or call
184 shell$ deploy --version
186 =head1 USAGE
188 deploy [--usage] [--help] [--man] [--version]
190 deploy [--command-line|-S <string>] [--debug|-D]
191 [--dir|--directory|-d <dirname>]
192 [--password|--pass|-p] [--prompt|-P]
193 [--script|-s <scriptname>] [--stderr|-E] [--stdout|-O]
194 [--username|--user|-u]
196 =head1 EXAMPLES
198 shell$ deploy
200 # Upload deploy-script.pl and execute it on each server listed
201 # in file "targets"
202 shell$ deploy -s deploy-script.pl `cat targets`
204 # ... without bugging me prompting confirmations...
205 shell$ deploy -s deploy-script.pl --no-prompt `cat targets`
207 # Execute a one-shot command remotely. Note UPPERCASE "s"
208 shell$ deploy -S 'ls -l /' `cat targets`
210 =head1 DESCRIPTION
212 This utility allows you to I<deploy> a script to one or more remote
213 hosts. Thus, you can provide a script that will be uploaded (via
214 B<sftp>) to the remote host and executed (via B<ssh>).
216 Before operations start for each host you will be prompted for
217 continuation: you can choose to go, skip or quit. You can disable
218 this by specifying C<--no-prompt>.
220 By default, directory C</tmp/our-deploy> on the target system will be
221 used. You can provide your own working directory path on the target system
222 via the C<--dir|--directory|-d> option. The directory will be created
223 if it does not exist.
225 For logging in, you can provide your own username/password pair directly
226 on the command line. Note that this utility explicitly avoids public
227 key authentication in favour of username/password authentication. Don't
228 ask me why, this may change in the future. Anyway, you're not obliged
229 to provide either on the command line: the username defaults to C<root>,
230 and you'll be prompted to provide a password if you don't put any
231 on the command line but specify the C<--password|-p> option without a value.
232 The prompt does not show the password on the terminal.
234 By default, L<Net::SSH::Perl> will try to use public/private key
235 authentication. If you're confident that this method will work, you can
236 just hit enter when requested for a password, or you can pass
237 C<-p> without a password on the command line (you can actually pass
238 every password you can think of, it will be ignored).
240 Starting from version 0.7.0, L<deploy> is also able to let you execute a
241 one-shot command remotely via the C<--command-line|-S> option; this lets
242 you avoid uploading a script and execute it and eases your life a bit if
243 you have to launch a single command, e.g.:
245 shell$ deploy -S 'ls /path/to/whatever' `cat targets`
247 In this case, nothing will be created in the target directory.
249 =head2 Output Format
251 The normal output format is geared at easing parsing by other programs. It
252 is compound of the following parts:
254 =over
256 =item *
258 a single line specifing the hostname/ip, with the following format:
260 *** OPERATING ON <hostname> ***
262 =item *
264 a single line reporting the exit code from the remote process, with the
265 following format:
267 </path/to/deployed/program> exit = <exit-code>
269 in case a script is uploaded, or the following format:
271 cmd[<command to be executed>] exit = <exit-code>
273 in case a single one-shot command is sent (see option C<--command-line|-S>).
275 =item *
277 0 or more lines starting with C<STDOUT > (note the space);
279 =item *
281 0 or more lines starting with C<STDERR > (note the space).
283 =back
285 If any of L</--stderr> or L</--stdout> are present, then the relevant
286 channel is printed on STDOUT immediately after the first two lines of the
287 format above, unchanged.
289 =head2 Example Runs
291 Suppose to have the following script F<bar.sh> to deploy:
293 #!/bin/bash
295 echo 'Hi there!'
296 ls baz
297 echo 'How are you all?!?'
299 If you don't provide any of L</--stderr> or L</--stdout>, you will have
300 something like this:
302 *** OPERATING ON foo.example.com ***
303 /tmp/our-deploy/bar.sh exit = 0
304 STDOUT Hi there!
305 STDOUT How are you all?!?
306 STDERR ls: baz: No such file or directory
308 If you pass L<--stderr> you will get:
310 *** OPERATING ON foo.example.com ***
311 /tmp/our-deploy/bar.sh exit = 0
312 ls: baz: No such file or directory
314 If you pass L<--stdout> you will get:
316 *** OPERATING ON foo.example.com ***
317 /tmp/our-deploy/bar.sh exit = 0
318 Hi there!
319 How are you all?!?
321 =head1 OPTIONS
323 =over
325 =item --command-line | -S
327 set a one-shot command to be executed instead of a script to be uploaded
328 and then executed. This option is incompatible with C<--script|-s>, because
329 with this you're requesting to execute a one-shot command, while with
330 that you're requesting to upload a file and then execute it.
332 =item --debug | -D
334 turns on debug mode, which should print out more stuff during operations.
335 You should not need it as a user.
337 =item --dir | --directory | -d <dirname>
339 specify the working directory on the target system. This is the
340 directory into which the deploy script will be uploaded. It will
341 be created if it does not exist.
343 Defaults to C</tmp/our-deploy>.
345 =item --help
347 print a somewhat more verbose help, showing usage, this description of
348 the options and some examples from the synopsis.
350 =item --man
352 print out the full documentation for the script.
354 =item --password | --pass | -p <password>
356 you can specify the password on the command line, even if it's probably
357 best B<NOT> to do so and wait for the program to prompt you one.
359 By default, you'll be prompted a password and this will not be written
360 on the terminal.
362 =item --prompt | -P
364 this option enables prompting before operations are started on each
365 host. When the prompt is enabled, you're presented with three choices:
367 =over
369 =item -
371 B<Yes> continue deployment on the given host;
373 =item -
375 B<Skip> skip this host;
377 =item -
379 B<No> stop deployment and exit.
381 =back
383 One letter suffices. By default, C<Yes> is assumed.
385 By default this option is I<always> active, so you're probably
386 interested in C<--no-prompt> to disable it.
388 =item --script | -s <scriptname>
390 set the script/program to upload and execute. This script will be uploaded
391 to the target system (see C<--directory|-d> above), but the name of the
392 script will be sanitised (only alphanumeric, C<_>, C<.> and C<-> will
393 be retained), so be careful if you have to look for the uploaded
394 script later.
396 This option is incompatible with C<--command-line|-S>.
398 =item --stderr | -E
400 select only the STDERR channel from the responses got via SSH. This
401 option cannot be used with L</--stdout>.
403 =item --stdout | -O
405 select only the STDOUT channel from the responses got via SSH. This
406 option cannot be used with L</--stderr>.
408 =item --usage
410 print a concise usage line and exit.
412 =item --username | --user | -u <username>
414 specify the user name to use for logging into the remote host(s).
416 Defaults to C<root>.
418 =item --version
420 print the version of the script.
422 =back
424 =head1 DIAGNOSTICS
426 =over
428 =item C<< no %s, sorry. Stopped at... >>
430 The given element is not available on the target system.
432 In case of the directory, this means that the automatic creation
433 process did not work for any reason. In case of the script, this
434 means that the file upload did not work.
436 =back
439 =head1 CONFIGURATION AND ENVIRONMENT
441 deploy requires no configuration files or environment variables.
444 =head1 DEPENDENCIES
446 =over
448 =item -
450 L<IO::Prompt>
452 =item -
454 L<Net::SFTP>
456 =item -
458 L<Net::SSH::Perl>
460 =back
463 =head1 BUGS AND LIMITATIONS
465 No bugs have been reported.
467 Please report any bugs or feature requests through http://rt.cpan.org/
470 =head1 AUTHOR
472 Flavio Poletti C<flavio@polettix.it>
475 =head1 LICENCE AND COPYRIGHT
477 Copyright (c) 2007-2008, Flavio Poletti C<flavio@polettix.it>.
478 All rights reserved.
480 This script is free software; you can redistribute it and/or
481 modify it under the same terms as Perl itself. See L<perlartistic>
482 and L<perlgpl>.
484 Questo script è software libero: potete ridistribuirlo e/o
485 modificarlo negli stessi termini di Perl stesso. Vedete anche
486 L<perlartistic> e L<perlgpl>.
489 =head1 DISCLAIMER OF WARRANTY
491 BECAUSE THIS SOFTWARE IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
492 FOR THE SOFTWARE, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
493 OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
494 PROVIDE THE SOFTWARE "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER
495 EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
496 WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE
497 ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE SOFTWARE IS WITH
498 YOU. SHOULD THE SOFTWARE PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL
499 NECESSARY SERVICING, REPAIR, OR CORRECTION.
501 IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
502 WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
503 REDISTRIBUTE THE SOFTWARE AS PERMITTED BY THE ABOVE LICENCE, BE
504 LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL,
505 OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE
506 THE SOFTWARE (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING
507 RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A
508 FAILURE OF THE SOFTWARE TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF
509 SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
510 SUCH DAMAGES.
512 =head1 NEGAZIONE DELLA GARANZIA
514 Poiché questo software viene dato con una licenza gratuita, non
515 c'è alcuna garanzia associata ad esso, ai fini e per quanto permesso
516 dalle leggi applicabili. A meno di quanto possa essere specificato
517 altrove, il proprietario e detentore del copyright fornisce questo
518 software "così com'è" senza garanzia di alcun tipo, sia essa espressa
519 o implicita, includendo fra l'altro (senza però limitarsi a questo)
520 eventuali garanzie implicite di commerciabilità e adeguatezza per
521 uno scopo particolare. L'intero rischio riguardo alla qualità ed
522 alle prestazioni di questo software rimane a voi. Se il software
523 dovesse dimostrarsi difettoso, vi assumete tutte le responsabilità
524 ed i costi per tutti i necessari servizi, riparazioni o correzioni.
526 In nessun caso, a meno che ciò non sia richiesto dalle leggi vigenti
527 o sia regolato da un accordo scritto, alcuno dei detentori del diritto
528 di copyright, o qualunque altra parte che possa modificare, o redistribuire
529 questo software così come consentito dalla licenza di cui sopra, potrà
530 essere considerato responsabile nei vostri confronti per danni, ivi
531 inclusi danni generali, speciali, incidentali o conseguenziali, derivanti
532 dall'utilizzo o dall'incapacità di utilizzo di questo software. Ciò
533 include, a puro titolo di esempio e senza limitarsi ad essi, la perdita
534 di dati, l'alterazione involontaria o indesiderata di dati, le perdite
535 sostenute da voi o da terze parti o un fallimento del software ad
536 operare con un qualsivoglia altro software. Tale negazione di garanzia
537 rimane in essere anche se i dententori del copyright, o qualsiasi altra
538 parte, è stata avvisata della possibilità di tali danneggiamenti.
540 Se decidete di utilizzare questo software, lo fate a vostro rischio
541 e pericolo. Se pensate che i termini di questa negazione di garanzia
542 non si confacciano alle vostre esigenze, o al vostro modo di
543 considerare un software, o ancora al modo in cui avete sempre trattato
544 software di terze parti, non usatelo. Se lo usate, accettate espressamente
545 questa negazione di garanzia e la piena responsabilità per qualsiasi
546 tipo di danno, di qualsiasi natura, possa derivarne.
548 =cut