Sanitised deploy script.
[deployable.git] / deployable
blob5f9e7c581e986a7aee767ec375b29ce0fbabf320
1 #!/usr/bin/env perl
2 use strict;
3 use warnings;
4 use Carp;
5 use version; our $VERSION = qv('0.0.1');
6 use Fatal qw( close );
7 use Pod::Usage qw( pod2usage );
8 use Getopt::Long qw( :config gnu_getopt );
9 use English qw( -no_match_vars );
10 use File::Basename qw( basename dirname );
11 use File::Spec::Functions qw( file_name_is_absolute catfile );
12 use POSIX qw( strftime );
13 use Cwd qw( cwd realpath );
14 use Archive::Tar;
15 use File::Find::Rule;
16 use Data::Dumper;
18 my %config = (
19 output => '-',
20 remote => catfile(dirname(realpath(__FILE__)), 'remote'),
21 tarfile => [],
22 heredir => [],
23 rootdir => [],
24 root => [],
25 deploy => [],
27 GetOptions(
28 \%config,
29 qw(
30 usage! help! man! version!
32 bundle|all-exec|X!
33 cleanup|c!
34 deploy|exec|d=s@
35 heredir|H=s@
36 include-archive-tar|T!
37 output|o=s
38 root|r=s@
39 rootdir|R=s@
40 workdir|work-directory|deploy-directory|w=s
43 pod2usage(message => "$0 $VERSION", -verbose => 99, -sections => '')
44 if $config{version};
45 pod2usage(-verbose => 99, -sections => 'USAGE') if $config{usage};
46 pod2usage(-verbose => 99, -sections => 'USAGE|EXAMPLES|OPTIONS')
47 if $config{help};
48 pod2usage(-verbose => 2) if $config{man};
50 pod2usage(
51 message => 'working directory must be an absolute path',
52 -verbose => 99,
53 -sections => ''
55 if exists $config{workdir} && !file_name_is_absolute($config{workdir});
57 if ($config{'include-archive-tar'}) {
58 $config{remote} = catfile(dirname(realpath(__FILE__)), 'remote-at');
59 if (! -e $config{remote}) { # "make" it
60 print {*STDERR} "### Making remote-at...\n";
61 my $startdir = cwd();
62 chdir dirname realpath __FILE__;
63 system {'make'} qw( make remote-at );
64 chdir $startdir;
68 # Where all the data will be kept
69 my $tar = Archive::Tar->new();
71 { # Add a configuration file
72 my %general_configuration;
73 for my $name (qw( workdir cleanup bundle deploy )) {
74 $general_configuration{$name} = $config{$name}
75 if exists $config{$name};
77 $tar->add_data('deployable/config.pl', Dumper \%general_configuration);
80 # Process files and directories. All these will be reported in the
81 # extraction directory, i.e. basename() will be applied to them. For
82 # directories, they will be re-created
83 for my $file (@ARGV) {
84 croak "'$file' not readable" unless -r $file;
85 if (-d $file) {
86 print {*STDERR} "processing directory '$file'\n";
87 save_directory($tar, '.', '.', 'here');
88 } ## end if (-d $file)
89 else {
90 print {*STDERR} "processing file '$file'\n";
91 save_file($tar, $file, 'here');
92 } ## end else [ if (-d $file)
93 } ## end for my $file (@ARGV)
95 # heredir-s are directories that are extracted directly into the ex dir
96 for my $heredir (@{$config{heredir}}) {
97 croak "'$heredir' not readable" unless -r $heredir;
98 print {*STDERR} "processing here-directory '$heredir'\n";
99 save_directory($tar, $heredir, '.', 'here');
100 } ## end for my $heredir (@{$config...
102 # rootdir-s are directories that will go under root
103 for my $rootdir (@{$config{rootdir}}) {
104 croak "'$rootdir' not readable" unless -r $rootdir;
105 print {*STDERR} "processing root-directory '$rootdir'\n";
106 save_directory($tar, '.', $rootdir, 'root');
109 # root-s are directories whose contents go under root
110 for my $root (@{$config{root}}) {
111 croak "'$root' not readable" unless -r $root;
112 print {*STDERR} "processing root-directory '$root'\n";
113 save_directory($tar, $root, '.', 'root');
116 # Establish output channel
117 my $out_fh = \*STDOUT;
118 if ($config{output} ne '-') {
119 open my $fh, '>', $config{output} ## no critic
120 or croak "open('$config{output}'): $OS_ERROR";
121 $out_fh = $fh;
123 binmode $out_fh;
125 # Emit script code to be executed remotely. It is guaranteed to end
126 # with __END__, so that all what comes next is data
127 print {$out_fh} get_remote_script();
129 # Save tar file, it will close the filehandle as well
130 $tar->write($out_fh);
132 # Set as executable
133 if ($config{output} ne '-') {
134 chmod oct(755), $config{output}
135 or carp "chmod(0755, '$config{output}'): $OS_ERROR";
139 my $cwd;
140 sub save_directory {
141 my ($tar, $localstart, $localdir, $remotestart) = @_;
143 $cwd ||= cwd();
144 chdir $localstart;
146 save_file($tar, $_, $remotestart) for File::Find::Rule->file()->in($localdir);
148 chdir $cwd;
150 return;
151 } ## end sub save_directory
154 sub save_file {
155 my ($tar, $filename, $remotestart) = @_;
157 my ($ftar) = $tar->add_files($filename);
158 $ftar->rename("$remotestart/$filename");
160 return;
161 } ## end sub save_file
163 sub get_remote_script {
164 open my $fh, '<', $config{remote}
165 or croak "open('$config{remote}'): $OS_ERROR";
166 my @lines;
167 while (<$fh>) {
168 last if /\A __END__ \s*\z/mxs;
169 push @lines, $_;
171 close $fh;
172 return join '', @lines, "__END__\n";
173 } ## end sub get_remote_script
175 __END__
177 =head1 NAME
179 deployable - create a deploy script for some files/scripts
181 =head1 VERSION
183 See version at beginning of script, variable $VERSION, or call
185 shell$ deployable --version
187 =head1 USAGE
189 deployable [--usage] [--help] [--man] [--version]
191 deployable [--bundle|--all-exec|-X] [--cleanup|-c]
192 [--deploy|--exec|d <program>] [--heredir|-H <dirname>]
193 [--include-archive-tar|-T] [--output|-o <filename>]
194 [--root|-r <dirname>] [--rootdir|-R <dirname>]
195 [--workdir|-w <path>] [ files or directories... ]
197 =head1 EXAMPLES
199 shell$ deployable
201 # Use a directory's contents as elements for the target root
202 shell$ ls -1 /path/to/target/root
207 # The above will be deployed as /etc, /opt, /usr and /var
208 shell$ deployable -o dep.pl --root /path/to/target/root
210 # Include directory /path/to/etc for inclusion and extraction
211 # directly as /etc
212 shell$ deployable -o dep.pl --rootdir /path/to/etc
214 =head1 DESCRIPTION
216 This is a meta-script to create deploy scripts. The latter ones are
217 suitable to be distributed in order to deploy something.
219 You basically have to provide two things: files to install and programs
220 to be executed. Files can be put directly into the deployed script, or
221 can be included in gzipped tar archives.
223 When called, this script creates a deploy script for you. This script
224 includes all the specified files, and when executed it will extract
225 those files and execute the given programs. In this way, you can ship
226 both files and logic needed to correctly install those files, but this
227 is of course of of scope.
229 All files and archives will be extracted under a configured path
230 (see L<--workdir> below), which we'll call I<workdir> from now on. Under
231 the I<workdir> a temporary directory will be created, and the files
232 will be put in the temporary directory. You can specify if you want to
233 clean up this temporary directory or keep it, at your choice. (You're able
234 to both set a default for this cleanup when invoking deployable, or when
235 invoking the deploy script itself). The temporary directory will be
236 called I<tmpdir> in the following.
238 There are several ways to embed files to be shipped:
240 =over
242 =item *
244 specify the file name directly on the command line. A file given in this
245 way will always be extracted into the I<tmpdir>, whatever its initial path
246 was.
248 =item *
250 specify the name of a directory on the command line. In this case,
251 C<tar> will be used to archive the directory, with the usual option to
252 turn absolute paths into relative ones; this means that directories will
253 be re-created under I<tmpdir> when extraction is performed.
255 =item *
257 give the name of a directory to be used as a "here directory", using
258 the C<--heredir|-H> option. This is much the same as giving the directory
259 name (see above), but in this case C<tar> will be told to change into the
260 directory first, and archive '.'. This means that the contents of the
261 "here-directory" will be extracted directly into I<tmpdir>.
263 =back
265 =head2 Extended Example
267 Suppose you have a few server which have the same configuration, apart
268 from some specific stuff (e.g. the hostname, the IP addresses, etc.).
269 You'd like to perform changes to all with the minimum work possible...
270 so you know you should script something.
272 For example, suppose you want to update a few files in /etc, setting these
273 files equal for all hosts. You would typically do the following:
275 # In your computer
276 shell$ mkdir -p /tmp/newfiles/etc
277 shell$ cd /tmp/newfiles/etc
278 # Craft the new files
279 shell$ cd ..
280 shell$ tar cvzf newetc.tar.gz etc
282 # Now, for each server:
283 shell$ scp newetc.tar.gz $server:/tmp
284 shell$ ssh $server tar xvzf /tmp/newetc.tar.gz -C /
287 So far, so good. But what if you need to kick in a little more logic?
288 For example, if you update some configuration files, you'll most likey
289 want to restart some services. So you could do the following:
291 shell$ mkdir -p /tmp/newfiles/tmp
292 shell$ cd /tmp/newfiles/tmp
293 # craft a shell script to be executed remotely and set the exec bit
294 # Suppose it's called deploy.sh
295 shell$ cd ..
296 shell$ tar cvzf newetc.tar.gz etc tmp
298 # Now, for each server:
299 shell$ scp newetc.tar.gz $server:/tmp
300 shell$ ssh $server tar xvzf /tmp/newetc.tar.gz -C /
301 shell$ ssh $server /tmp/deploy.sh
303 And what if you want to install files depending on the particular machine?
304 Or you have a bundle of stuff to deploy and a bunch of scripts to execute?
305 You can use deployable. In this case, you can do the following:
307 shell$ mkdir -p /tmp/newfiles/etc
308 shell$ cd /tmp/newfiles/etc
309 # Craft the new files
310 shell$ cd ..
311 # craft a shell script to be executed remotely and set the exec bit
312 # Suppose it's called deploy.sh
313 shell$ deployable -o deploy.pl etc deploy.sh --exec deploy.sh
315 # Now, for each server
316 shell$ scp deploy.pl $server:/tmp
317 shell$ ssh $server /tmp/deploy.pl
319 And you're done. This can be particularly useful if you have another
320 layer of deployment, e.g. if you have to run a script to decide which
321 of a group of archives should be deployed. For example, you could craft
322 a different new "etc" for each server (which is particularly true if
323 network configurations are in the package), and produce a simple script
324 to choose which file to use based on the MAC address of the machine. In
325 this case you could have:
327 =over
329 =item newetc.*.tar.gz
331 a bunch of tar files with the configurations for each different server
333 =item newetc.list
335 a list file with the association between the MAC addresses and the
336 real tar file to deploy from the bunch in the previous bullet
338 =item deploy-the-right-stuff.sh
340 a script to get the real MAC address of the machine, select the right
341 tar file and do the deployment.
343 =back
345 So, you can do the following:
347 shell$ deployable -o deploy.pl newetc.*.tar.gz newetc.list \
348 deploy-the-right-stuff.sh --exec deploy-the-right-stuff.sh
350 # Now, for each server:
351 shell$ scp deploy.pl $server:/tmp
352 shell$ ssh $server /tmp/deploy.pl
354 So, once you have the deploy script on the target machine all you need
355 to do is to execute it. This can come handy when you cannot access the
356 machines from the network, but you have to go there physically: you
357 can prepare all in advance, and just call the deploy script.
360 =head1 OPTIONS
362 Meta-options:
364 =over
366 =item B<--help>
368 print a somewhat more verbose help, showing usage, this description of
369 the options and some examples from the synopsis.
371 =item B<--man>
373 print out the full documentation for the script.
375 =item B<--usage>
377 print a concise usage line and exit.
379 =item B<--version>
381 print the version of the script.
383 =back
385 Real-world options:
387 =over
389 =item B<< --bundle | --all-exec | -X >>
391 Set bundle flag in the produced script. If the bundle flag is set, the
392 I<deploy script> will treat all executables in the main deployment
393 directory as scripts to be executed.
395 By default the flag is not set.
397 =item B<< --cleanup | -c >>
399 Set cleanup flag in the produced script. If the cleanup flag is set, the
400 I<deploy script> will clean up after having performed all operations.
402 You can set this flag to C<0> by using C<--no-cleanup>.
404 =item B<< --deploy | --exec | -d <filename> >>
406 Set the name of a program to execute after extraction. You can provide
407 multiple program names, they will be executed in the same order.
409 =item B<< --heredir | -H <path> >>
411 Set the name of a "here directory" (see L<DESCRIPTION>). You can use this
412 option multiple times to provide multiple directories.
414 =item B<< --include-archive-tar | -T >>
416 Embed L<Archive::Tar> (with its dependencies L<Archive::Tar::Constant> and
417 L<Archive::Tar::File>) inside the final script. Use this when you know (or
418 aren't sure) that L<Archive::Tar> will not be available in the target
419 machine.
421 =item B<< --output | -o <filename> >>
423 Set the output file name. By default the I<deploy script> will be given
424 out on the standard output; if you provide a filename (different from
425 C<->, of course!) the script will be saved there and the permissions will
426 be set to 0755.
428 =item B<< --root | -r <dirname> >>
430 Include C<dirname> contents for deployment under root directory. The
431 actual production procedure is: hop into C<dirname> and grab a tarball
432 of C<.>. During deployment, hop into C</> and extract the tarball.
434 This is useful if you're already building up the absolute deployment
435 layout under a given directory: just treat that directory as if it were
436 the root of the target system.
438 =item B<< --rootdir | -R <dirname >>
440 Include C<dirname> as a directory that will be extracted under root
441 directory. The actual production procedure is: grab a tarball of
442 C<dirname>. During deployment, hop into C</> and extract the tarball.
444 This is useful if you have a directory (or a group of directories) that
445 you want to deploy directly under the root.
447 =item B<< --workdir | --deploy-directory | -w <path> >>
449 Set the working directory for the deploy.
451 =back
453 =head1 THE DEPLOY SCRIPT
455 The net result of calling this script is to produce another script,
456 that we call the I<deploy script>. This script is made of two parts: the
457 code, which is fixed, and the configurations/files, which is what is
458 actually produced. The latter part is put after the C<__END__> marker,
459 as usual.
461 Stuff in the configuration part is always hexified in order to prevent
462 strange tricks or errors. Comments will help you devise what's inside the
463 configurations themselves.
465 The I<deploy script> has options itself, even if they are quite minimal.
466 In particular, it supports the same options C<--workdir|-w> and
467 C<--cleanup> described above, allowing the final user to override the
468 configured values. By default, the I<workdir> is set to C</tmp/our-deploy>
469 and the script will clean up after itself.
471 The following options are supported in the I<deploy script>:
473 =over
475 =item B<--usage | --man | --help>
477 print a minimal help and exit
479 =item B<--version>
481 print script version and exit
483 =item B<--bundle | --all-exec | -X>
485 treat all executables in the main deployment directory as scripts
486 to be executed
488 =item B<--cleanup | --no-cleanup>
490 perform / don't perform temporary directory cleanup after work done
492 =item B<--dryrun | --dry-run>
494 print final options and exit
496 =item B<< --inspect <dirname> >>
498 just extract all the stuff into <dirname> for inspection. Implies
499 C<--no-deploy>, C<--no-tempdir>, ignores C<--bundle> (as a consequence of
500 C<--no-deploy>), disables C<--cleanup> and sets the working directory
501 to C<dirname>
503 =item B<--no-deploy>
505 prevent execution of deploy scripts (they are executed by default)
507 =item B<--no-workdir>
509 execute directly in workdir (see below), without creating the
510 temporary directory
512 =item B<--show | --show-options | -s>
514 print configured options and exit
516 =item B<--workdir | -w>
518 working base directory (a temporary subdirectory will be created
519 there anyway)
521 =back
523 Note the difference between C<--show> and C<--dryrun>: the former will
524 give you the options that are "embedded" in the I<deploy script> without
525 taking into account other options given on the command line, while the
526 latter will give you the final options that would be used if the script
527 were called without C<--dryrun>.
529 =head2 Deploy Script Example Usage
531 In the following, we'll assume that the I<deploy script> is called
532 C<deploy.pl>.
534 To execute the script with the already configured options, you just have
535 to call it:
537 shell$ ./deploy.pl
539 If you just want to see which configurations are in the I<deploy script>:
541 shell$ ./deploy.pl --show
543 Extract contents of the script in a temp directory and simply inspect
544 what's inside:
546 # extract stuff into subdirectory 'inspect' for... inspection
547 shell$ ./deploy.pl --no-tempdir --no-deploy --workdir inspect
549 =head2 Deploy Script Requirements
551 Care has been taken to make the requirements of the deploy script as low
552 as possible. The result is that you'll need a working Perl with version
553 at least 5.6.2, and GNU B<tar> with support for options C<--touch> and
554 C<--no-same-owner>. Good luck.
557 =head1 DIAGNOSTICS
559 Each error message should be enough explicit to be understood without the
560 need for furter explainations. Which is another way to say that I'm way
561 too lazy to list all possible ways that this script has to fail.
564 =head1 CONFIGURATION AND ENVIRONMENT
566 deployable requires no configuration files or environment variables.
568 Please note that deployable B<needs> to find its master B<remote> file
569 to produce the final script. This must be put in the same directory where
570 deployable is put. You should be able to B<symlink> deployable where you
571 think it's better, anyway - it will go search for the original file
572 and look for B<remote> inside the same directory. This does not apply to
573 hard links, of course.
576 =head1 DEPENDENCIES
578 All core modules, apart from L<version> which is nearly-core.
581 =head1 BUGS AND LIMITATIONS
583 No bugs have been reported.
585 Please report any bugs or feature requests to the AUTHOR below.
588 =head1 AUTHOR
590 Flavio Poletti C<flavio [AT] polettix.it>
593 =head1 LICENSE AND COPYRIGHT
595 Copyright (c) 2008, Flavio Poletti C<flavio [AT] polettix.it>. All rights reserved.
597 This script is free software; you can redistribute it and/or
598 modify it under the same terms as Perl itself. See L<perlartistic>
599 and L<perlgpl>.
601 Questo script è software libero: potete ridistribuirlo e/o
602 modificarlo negli stessi termini di Perl stesso. Vedete anche
603 L<perlartistic> e L<perlgpl>.
606 =head1 DISCLAIMER OF WARRANTY
608 BECAUSE THIS SOFTWARE IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
609 FOR THE SOFTWARE, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
610 OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
611 PROVIDE THE SOFTWARE "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER
612 EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
613 WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE
614 ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE SOFTWARE IS WITH
615 YOU. SHOULD THE SOFTWARE PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL
616 NECESSARY SERVICING, REPAIR, OR CORRECTION.
618 IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
619 WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
620 REDISTRIBUTE THE SOFTWARE AS PERMITTED BY THE ABOVE LICENCE, BE
621 LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL,
622 OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE
623 THE SOFTWARE (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING
624 RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A
625 FAILURE OF THE SOFTWARE TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF
626 SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
627 SUCH DAMAGES.
629 =head1 NEGAZIONE DELLA GARANZIA
631 Poiché questo software viene dato con una licenza gratuita, non
632 c'è alcuna garanzia associata ad esso, ai fini e per quanto permesso
633 dalle leggi applicabili. A meno di quanto possa essere specificato
634 altrove, il proprietario e detentore del copyright fornisce questo
635 software "così com'è" senza garanzia di alcun tipo, sia essa espressa
636 o implicita, includendo fra l'altro (senza però limitarsi a questo)
637 eventuali garanzie implicite di commerciabilità e adeguatezza per
638 uno scopo particolare. L'intero rischio riguardo alla qualità ed
639 alle prestazioni di questo software rimane a voi. Se il software
640 dovesse dimostrarsi difettoso, vi assumete tutte le responsabilità
641 ed i costi per tutti i necessari servizi, riparazioni o correzioni.
643 In nessun caso, a meno che ciò non sia richiesto dalle leggi vigenti
644 o sia regolato da un accordo scritto, alcuno dei detentori del diritto
645 di copyright, o qualunque altra parte che possa modificare, o redistribuire
646 questo software così come consentito dalla licenza di cui sopra, potrà
647 essere considerato responsabile nei vostri confronti per danni, ivi
648 inclusi danni generali, speciali, incidentali o conseguenziali, derivanti
649 dall'utilizzo o dall'incapacità di utilizzo di questo software. Ciò
650 include, a puro titolo di esempio e senza limitarsi ad essi, la perdita
651 di dati, l'alterazione involontaria o indesiderata di dati, le perdite
652 sostenute da voi o da terze parti o un fallimento del software ad
653 operare con un qualsivoglia altro software. Tale negazione di garanzia
654 rimane in essere anche se i dententori del copyright, o qualsiasi altra
655 parte, è stata avvisata della possibilità di tali danneggiamenti.
657 Se decidete di utilizzare questo software, lo fate a vostro rischio
658 e pericolo. Se pensate che i termini di questa negazione di garanzia
659 non si confacciano alle vostre esigenze, o al vostro modo di
660 considerare un software, o ancora al modo in cui avete sempre trattato
661 software di terze parti, non usatelo. Se lo usate, accettate espressamente
662 questa negazione di garanzia e la piena responsabilità per qualsiasi
663 tipo di danno, di qualsiasi natura, possa derivarne.
665 =cut