source: ProjectBuilder/projects/casparbuster/devel/bin/cb@ 1683

Last change on this file since 1683 was 1683, checked in by Bruno Cornec, 11 years ago
  • Fix tar build to include also common files
  • Use ssh instead of Net:SSH2 to lauch the final setup script as it wasn't working for a reason
  • Use sudo to lauch these scripts, which require norequiretty fin sudoers for the account used
  • Property svn:executable set to *
File size: 15.4 KB
RevLine 
[1466]1#!/usr/bin/perl -w
2#
3=head1 NAME
4
[1485]5cb - CasparBuster looks at the structure in your CMS environment and deploy it to the target systems as needed
[1466]6
7=head1 SYNOPSIS
8
[1485]9cb [options]
[1466]10
[1485]11 Options:
12 --debug |-d debug mode
13 --help |-h brief help message
14 --man full documentation
[1489]15 --force |-f force copy of files, even if they exist
[1485]16 --source |-s <file/dir> directory or files to copy from the CasparBuster tree (',' separated if many) to the target
17 --plugin |-p <plugin name> plugin defining what to copy from the CasparBuster tree (',' separated if many) to the target
18 --machine|-m <machine> machine to deploy on.
[1466]19
20=head1 OPTIONS
21
22=over 4
23
[1485]24=item B<--debug>
[1466]25
[1485]26Enter debug mode. This will print what would be done. No commands are executed,
27so this is safe to use when testing.
[1466]28
[1485]29=item B<--help>
[1466]30
31Print a brief help message and exits.
32
33=item B<--man>
34
35Prints the manual page and exits.
36
[1485]37=item B<--machine> I<machine name>
[1466]38
[1485]39Specify the machine to consider when dealing with the CasparBuster structure.
40The files will be pushed to this machine, and a subdirectory named after the machine
41will be used under the basedir to look at the directory structure to deploy
[1662]42When no machine is given, all machnes available are processed
[1466]43
[1485]44=item B<--source> I<path>
[1466]45
[1485]46Specify the path of the source file or directory to deploy with CasparBuster. Multiple paths can be specified separated by ','.
[1466]47
[1485]48=item B<--plugin> I<name>
[1466]49
[1485]50Specify the name of the plugin to deploy with CasparBuster. Multiple plugins can be specified separated by ','.
51A plugin defines a set of files (with their mode and owner), a set of directories (with their mode and owner) and a set of scripts to launch once the files are copied remotely.
[1466]52
[1485]53=back
[1466]54
[1485]55=head1 DESCRIPTION
[1466]56
[1485]57Deploy the standard CasparBuster structure created by I<cbusterize>. It will reinstall all files and directory in the plugin, with correct owner, group and mode, and launch at the end the script to re-enable potentially the service using the updated files.
[1466]58
[1485]59=head1 EXAMPLES
[1466]60
[1485]61 # this will deploy the appropriate CasparBuster environment for DHCP
62 # from the base ~/prj/musique-ancienne.org directory (Cf cbbasedir in cb.conf)
63 # containing the directory victoria2 for this machine
64 # to which it will copy the required files
[1466]65
[1485]66 cb -m victoria2 -p dhcpd
[1466]67
[1485]68=head1 AUTHOR
[1466]69
70=over 4
71
[1485]72Bruno Cornec, http://brunocornec.wordpress.com
[1466]73
[1485]74=back
[1466]75
[1485]76=head1 LICENSE
[1466]77
[1485]78Copyright (C) 2012 Bruno Cornec <bruno@project-builder.org>
79Released under the GPLv2 or the Artistic license at your will.
[1466]80
81=cut
[1485]82use strict;
83use CasparBuster::Version;
84use CasparBuster::Env;
85use CasparBuster::Plugin;
[1667]86use CasparBuster::SSH;
[1485]87#use Cwd 'realpath';
[1662]88use Carp qw/confess cluck/;
[1485]89use File::Find;
[1487]90use Archive::Tar;
[1485]91use Getopt::Long;
92use Pod::Usage;
93use Data::Dumper;
[1490]94use Time::Local;
95use Net::SSH2;
[1485]96use ProjectBuilder::Base;
97use ProjectBuilder::Conf;
98use ProjectBuilder::VCS;
99use DBI;
100use DBD::SQLite;
[1466]101
[1485]102# settings
103my $debug = 0;
104my $help = undef;
105my $man = undef;
106my $source = undef;
107my $machine = undef;
108my $plugin = undef;
109my $quiet = undef;
110my $force = undef;
111my $log = undef;
112my $LOG = undef;
[1662]113my $findtarget = undef;
[1466]114
[1485]115my ($cbver,$cbrev) = cb_version_init();
116my $appname = "cb";
117$ENV{'PBPROJ'} = $appname;
[1466]118
119# Initialize the syntax string
[1485]120pb_syntax_init("$appname (aka CasparBuster) Version $cbver-$cbrev\n");
[1466]121
[1485]122# parse command-line options
123GetOptions(
124 'machine|m=s' => \$machine,
125 'debug|d+' => \$debug,
126 'help|h' => \$help,
127 'quiet|q' => \$quiet,
128 'force|f' => \$force,
129 'man' => \$man,
130 'logfile|l=s' => \$log,
131 'source|s=s' => \$source,
132 'plugin|p=s' => \$plugin,
[1466]133) || pb_syntax(-1,0);
134
[1485]135if (defined $help) {
[1466]136 pb_syntax(0,1);
137}
[1485]138if (defined $man) {
[1466]139 pb_syntax(0,2);
140}
[1485]141if (defined $quiet) {
142 $debug=-1;
[1466]143}
[1485]144if (defined $log) {
145 open(LOG,"> $log") || die "Unable to log to $log: $!";
146 $LOG = \*LOG;
147 $debug = 0 if ($debug == -1);
[1466]148}
149
[1485]150pb_log_init($debug, $LOG);
[1662]151pb_temp_init();
152pb_log(1,"Starting cb\n");
[1466]153
[1485]154# Get conf file in context
155pb_conf_init($appname);
156# The personal one if there is such
157pb_conf_add("$ENV{'HOME'}/.cbrc") if (-f "$ENV{'HOME'}/.cbrc");
158# The system one
159pb_conf_add(cb_env_conffile());
[1466]160
[1485]161# Get configuration parameters
162my %cb;
[1489]163my $cbp = ();
[1485]164my $cb = \%cb;
[1662]165($cb->{'basedir'},$cb->{'cms'},$cb->{'database'}) = pb_conf_get("cbbasedir","cbcms","cbdatabase");
[1485]166pb_log(2,"%cb: ",Dumper($cb));
[1466]167
[1485]168if (defined $plugin) {
169 # Load plugins
170 cb_plugin_load();
[1466]171}
172
[1485]173my $basedir = $cb->{'basedir'}->{$appname};
174eval { $basedir =~ s/(\$ENV.+\})/$1/eeg };
[1466]175
[1485]176# Create basedir if it doesn't exist
177die "Unable to find base directory at $basedir" if (not -d $basedir);
[1466]178
[1487]179pb_log(1, "DEBUG MODE, not doing anything, just printing\nDEBUG: basedir = $basedir\n");
[1466]180
[1487]181# Create database if not existing and give a handler
182my $db = "$basedir/$cb->{'database'}->{$appname}";
[1466]183
[1485]184my $precmd = "";
185if (! -f $db) {
[1487]186 $precmd = "CREATE TABLE dates (id INTEGER PRIMARY KEY AUTOINCREMENT, date INTEGER, file VARCHAR[65535], machine VARCHAR[65535])";
[1466]187}
188
[1485]189my $dbh = DBI->connect("dbi:SQLite:dbname=$db","","",
190 { RaiseError => 1, AutoCommit => 1 })
191 || die "Unable to connect to $db";
[1490]192my $sth;
[1466]193
[1485]194if ($precmd ne "") {
[1490]195 $sth = $dbh->prepare(qq{$precmd}) || die "Unable to create table into $db";
[1485]196 if ($debug) {
[1487]197 pb_log(1,"DEBUG: Creating DB $db\n");
198 pb_log(1,"DEBUG: with command $precmd\n");
[1466]199 } else {
[1485]200 $sth->execute();
[1466]201 }
[1487]202 $sth->finish();
[1466]203}
204
[1487]205# Define destination dir and populate with a VCS export
[1662]206my $dest = "$ENV{'PBTMP'}/vcs.$$";
[1487]207my $scheme = $cb->{'cms'}->{$appname};
[1662]208
209# Avoids too many permission changes
210umask(0022);
[1487]211pb_vcs_export(pb_vcs_get_uri($scheme,$basedir),$basedir,$dest);
[1466]212
[1487]213# Now distribute to the right machines
[1489]214if (defined $machine) {
[1487]215 cb_distribute($machine);
[1485]216} else {
[1662]217 # Distribute to all
218 # First dir level is the machine, then the content
219 opendir(DIR,$dest) || die "Unable to open $dest: $!";
220 foreach my $m (readdir(DIR)) {
221 next if ($m =~ /^\./);
222 next if (! -d $m);
223 # Machine name
224 cb_distribute($m);
225 closedir(DIR);
[1466]226 }
227}
228
[1487]229# Cleanup
230$dbh->disconnect;
[1662]231pb_exit();
[1487]232
233# End of Main
234
235# Distribute files to target machines
236sub cb_distribute {
237
238my $machine = shift;
239
240pb_log(2,"Entering into cb_distribute with machine $machine\n");
[1662]241confess "No machine fiven to cb_distribute" if (not defined $machine);
[1487]242
243# Use potentially a remote account if defined
[1490]244my $remote = undef;
[1662]245my ($account) = pb_conf_get_if("cbaccount");
246$remote = $account->{$machine} if ((defined $account) && (defined $account->{$machine}));
[1490]247pb_log(1, "DEBUG: remote account1 = $remote\n") if (defined $remote);
248$remote = getpwuid($<) if (not defined $remote);
249pb_log(1, "DEBUG: remote account2 = $remote\n");
[1487]250
251# Now handle plugins if any
252if (defined $plugin) {
253 foreach my $p (split(/,/,$plugin)) {
254 pb_log(1,"Getting context for plugin $p\n");
255 $cbp = cb_plugin_get($p,$cbp);
256 # Adds mtime info to the plugin structure
[1494]257 foreach my $type ('files','dirs','dirsandfiles') {
[1487]258 foreach my $f (keys %{$cbp->{$p}->{$type}}) {
[1662]259 my $tdir = "$dest/$machine";
[1487]260 if (-r "$tdir/$f") {
261 my ($dev,$ino,$mode,$nlink,$uid,$gid,$rdev,$size,$atime,$mtime,$ctime,$blksize,$blocks) = stat("$tdir/$f") || die "Unable to stat $tdir: $!";
262 $cbp->{$p}->{$type}->{$f}->{'mtime'} = $mtime;
263 } else {
264 pb_log(0,"WARNING: Unable to read $tdir/$f from plugin $p\n");
265 }
266 }
267 }
268 }
[1466]269}
270
[1487]271# Handle this source
272if (defined $source) {
273 my $fullsource = "$source";
[1662]274 $fullsource = "$machine/$source";
[1487]275 pb_log(2,"fullsource is $fullsource\n");
276 my ($dev,$ino,$mode,$nlink,$uid,$gid,$rdev,$size,$atime,$mtime,$ctime,$blksize,$blocks) = stat("$dest/$fullsource") || die "Unable to stat $fullsource: $!";
277 my $type = 'files';
278 if (-d $fullsource) {
[1494]279 $type = 'dirsandfiles';
[1487]280 }
281 die "ERROR: Only able to handle files or dirs with option --source\n" if ((! -d $fullsource) && (! -f $fullsource));
[1466]282
[1679]283 # We should get uid/gid from elsewhere as they're probably wrong locally
[1487]284 $cbp->{"cb.source"}->{$type}->{$source}->{'uid'} = $uid;
285 $cbp->{"cb.source"}->{$type}->{$source}->{'gid'} = $gid;
286 $cbp->{"cb.source"}->{$type}->{$source}->{'mode'} = $mode;
287 $cbp->{"cb.source"}->{$type}->{$source}->{'mtime'} = $mtime;
288}
[1466]289
[1662]290($cb->{'commondir'},$cb->{'websrv'},$cb->{'webdir'}) = pb_conf_get_if("cbcommondir","cbwebsrv","cbwebdir");
291
[1487]292if ((not defined $source) && (not defined $plugin)) {
293 # Here we need to take all content under $dest considering that machine
[1662]294 $findtarget = "$dest/$machine";
295 find(\&cb_add_to_cbp,($findtarget));
[1679]296 # And we also need all what is common, but not what is for the web side
297 foreach my $c (keys $cb->{'commondir'}) {
298 $findtarget = "$dest/$c";
299 opendir(DIR,"$findtarget") || die "Unable to open $dest/$c: $!";
300 foreach my $m (readdir(DIR)) {
301 next if ($m =~ /^\./);
302 next if ($m eq $cb->{'commondir'}->{$c});
303 find(\&cb_add_to_cbp,("$findtarget/$m"));
304 }
305 closedir(DIR);
306 }
[1466]307}
[1662]308pb_log(1,"INFO: RAW cbp: ".Dumper(%$cbp)."\n");
[1466]309
[1487]310# Clean up cbp structure by comparing with data stored in the DB
311# Only keep the more recent modified content
[1490]312# Allow for errors to occur at DBI level
313$dbh->{RaiseError} = 0;
314my $checkdb = 1;
[1662]315my $dbcmd = "SELECT id,date,file,machine FROM dates WHERE machine=\"$machine\"";
[1490]316if (! ($sth = $dbh->prepare(qq{$dbcmd}))) {
317 pb_log(0,"Unable to prepare DB statement $dbcmd\n");
318 $checkdb = 0;
319}
320# DisAllow for errors to occur at DBI level
321$dbh->{RaiseError} = 1;
322my $dbid = ();
323if ($checkdb == 1) {
324 $sth->execute();
[1662]325 # Check what in cbp is in the DB and deploy only if necessary or forced
[1490]326 foreach my $k (keys %{$cbp}) {
[1494]327 foreach my $type ('files','dirs','dirsandfiles') {
[1490]328 foreach my $o (keys %{$cbp->{$k}->{$type}}) {
329 # Compare with info from DB
330 foreach my $row ($sth->fetch) {
331 next if (not defined $row);
332 my ($id, $date, $file, $mac1) = @$row;
333 # If less recent than in the DB remove it
334 $cbp->{$k}->{$type}->{$o}->{'deleted'} = "true" if ((defined $file) && ($file eq $o) && ($date > $cbp->{$k}->{$type}->{$o}->{'mtime'}));
335 $dbid->{$o} = $id;
336 }
[1487]337 }
338 }
339 }
[1490]340 $sth->finish();
[1487]341}
342pb_log(2,"INFO: cleaned cbp: ".Dumper($cbp)."\n");
[1466]343
[1487]344# Now create a tar containing all the relevant content
[1490]345# We need to loop separately to allow for DB to not exist in the previous loop !
346my $tdir = undef;
[1662]347$tdir = "$dest/$machine";
[1490]348chdir("$tdir") || die "ERROR: Unable to chdir to $tdir\n";
349pb_log(2,"Working now under $tdir\n");
350
[1487]351my $tar = Archive::Tar->new;
[1490]352my $curdate = time();
[1489]353foreach my $k (keys %{$cbp}) {
[1494]354 foreach my $type ('files','dirs','dirsandfiles') {
355 # TODO: for dirs we may remove the files below ?
[1490]356 foreach my $o ((keys %{$cbp->{$k}->{$type}})) {
[1662]357 if ((defined $force) || (not defined $cbp->{$k}->{$type}->{$o}->{'deleted'})) {
[1683]358 if ( -r "$tdir/$o" ) {
359 pb_log(1,"INFO: Adding to the tar file $tdir/$o\n");
360 chdir($tdir);
361 $tar->add_files("$o");
362 } else {
363 # It's in the common place instead
364 foreach my $c (keys $cb->{'commondir'}) {
365 if (-r "$dest/$c/$o") {
366 pb_log(1,"INFO: Adding to the tar file $dest/$c/$o\n");
367 chdir("$dest/$c");
368 $tar->add_files("$o");
369 }
370 }
371 }
[1490]372 # Add an entry to the DB
373 if (defined $dbid->{$o}) {
374 # Modify an existing entry
[1662]375 $dbcmd = "UPDATE dates SET date=\"$curdate\",file=\"$o\" WHERE id=\"$dbid->{$o}\"";
[1490]376 if (not $debug) {
377 $sth = $dbh->prepare(qq{$dbcmd});
[1662]378 $sth->execute();
[1490]379 }
[1662]380 pb_log(0,"Executing in DB: $dbcmd with curdate=$curdate,file=$o,id=$dbid->{$o}\n");
[1490]381 } else {
382 # Add an new entry
[1662]383 $dbcmd = "INSERT INTO dates VALUES (NULL,?,?,\"$machine\")";
[1490]384 if (not $debug) {
385 $sth = $dbh->prepare(qq{$dbcmd});
[1662]386 $sth->execute($curdate,$o);
[1490]387 }
[1662]388 pb_log(0,"Executing in DB: $dbcmd with curdate=$curdate,file=$o,machine=$machine\n");
[1490]389 }
390 if (not $debug) {
391 $sth->finish();
392 }
393 }
394 }
395 }
[1489]396}
[1662]397my $tarfile = "$ENV{'PBTMP'}/cbcontent$$.tar";
[1489]398$tar->write("$tarfile");
[1466]399
[1490]400my $ssh2;
401my $chan;
402
[1662]403my $mach = $machine;
404if ((defined $cb->{'commondir'}) && (defined $cb->{'commondir'}->{$machine})) {
405 confess "Please provide a cbwebsrv config parameter in order to use common delivery" if ((not defined $cb->{'websrv'}) && (not defined $cb->{'websrv'}->{$machine}));
406 $mach = $cb->{'websrv'}->{$machine};
[1490]407}
408
[1682]409$ssh2 = cb_ssh_init($remote,$mach,$debug);
[1466]410
[1683]411if (!($ssh2->scp_put($tarfile,$tarfile))) {
412 my @error = $ssh2->error();
413 print "@error\n";
414 confess "Unable to copy tar file $tarfile to $mach\n";
415}
[1667]416pb_log(0,"INFO: Copying content under $ENV{'PBTMP'} on $remote\@$mach\n");
417
[1662]418my $path = "/";
419my $tbextract = "";
420if ((defined $cb->{'commondir'}) && (defined $cb->{'commondir'}->{$machine})) {
421 $path = $cb->{'webdir'}->{$machine};
[1683]422 #$tbextract = $cb->{'commondir'}->{$machine};
[1662]423}
424$chan = $ssh2->channel();
[1667]425confess "Unable to launch remote shell through Net:SSH2 ($remote\@$mach)" if (not $chan->shell());
426
[1490]427if (not $debug) {
[1683]428 # Reminder: sudo should be configured for this account as Defaults !requiretty for this to work
[1662]429 print $chan "sudo tar -C $path -x -f $tarfile $tbextract\n";
430 pb_log(0,"WARNING: $_\n") while (<$chan>);
[1466]431} else {
[1662]432 print $chan "tar -C $path -t -f $tarfile $tbextract\n";
433 pb_log(2,"INFO: tar content: $_") while (<$chan>);
[1466]434}
[1490]435
[1682]436pb_log(0,"INFO: Extracting $tbextract (on $mach) $tarfile under $path\n");
[1662]437
[1490]438foreach my $k (keys %{$cbp}) {
[1494]439 foreach my $type ('files','dirs','dirsandfiles') {
440 # TODO: do we act recursively for dirsandfiles at least for uid/gid ?
[1490]441 foreach my $o ((keys %{$cbp->{$k}->{$type}})) {
[1662]442 # Note that $path/$o is remote only
443 if ((defined $force) || (not defined $cbp->{$k}->{$type}->{$o}->{'deleted'})) {
[1490]444 if ($debug) {
[1682]445 #pb_log(1,"INFO: Executing (on $mach) sudo chown $cbp->{$k}->{$type}->{$o}->{'uid'}:$cbp->{$k}->{$type}->{$o}->{'gid'} $path/$o\n");
446 #pb_log(1,"INFO: Executing (on $mach) sudo chmod $cbp->{$k}->{$type}->{$o}->{'mode'} $path/$o\n");
[1490]447 } else {
[1662]448 # TODO: remove hardcoded commands
[1679]449 #print $chan "sudo chown $cbp->{$k}->{$type}->{$o}->{'uid'}:$cbp->{$k}->{$type}->{$o}->{'gid'} $path/$o\n";
[1664]450 # TODO: get a correct mode before setting it up
451 #print $chan "sudo chmod $cbp->{$k}->{$type}->{$o}->{'mode'} $path/$o\n";
[1490]452 }
[1682]453 pb_log(0,"INFO: Delivering $path/$o on $mach\n");
[1490]454 }
455 }
456 }
457 if (defined $cbp->{$k}->{'reloadscript'}) {
[1662]458 if (not $debug) {
[1683]459 print $chan "sudo $cbp->{$k}->{'reloadscript'}\n";
[1490]460 }
[1683]461 pb_log(0,"INFO: Executing (on $mach) $cbp->{$k}->{'reloadscript'} as root\n");
[1490]462 }
463}
464
[1683]465pb_log(0,"INFO: Executing (on $mach) /usr/local/bin/mk$mach if present as root\n");
466if (not $debug) {
467 # Using Net::SSH2 here was not working (due to the shell ?)
468 pb_system("ssh $remote\@$mach \"sudo /usr/local/bin/mk$mach\"","WAIT: Executing (on $mach) /usr/local/bin/mk$mach if present as root","verbose");
469}
470
[1662]471# Remote cleanup
[1490]472if (not $debug) {
[1662]473 print $chan "rm -rf $ENV{'PBTMP'}\n";
[1490]474} else {
[1683]475 pb_log(1,"INFO: Please remove remote directory $ENV{'PBTMP'} on $mach\n");
[1490]476}
[1662]477$chan->close();
478
[1667]479cb_ssh_close($ssh2);
480
[1662]481chdir("/");
[1487]482pb_log(2,"Exiting cb_distribute\n");
[1466]483}
[1487]484
485sub cb_add_to_cbp {
486
[1662]487pb_log(3,"Entering into cb_add_to_cbp\n");
[1487]488my $type = 'files';
489if (-d $File::Find::name) {
490 $type = 'dirs';
491}
492
[1662]493# Target name is without the $findtarget part
494my $targetname = $File::Find::name;
495$targetname =~ s|^$findtarget[/]*||;
496
497return if ($targetname eq "");
498
499my ($dev,$ino,$mode,$nlink,$uid,$gid,$rdev,$size,$atime,$mtime,$ctime,$blksize,$blocks) = stat($File::Find::name);
500die "Unable to stat $File::Find::name" if (not defined $mode);
501# We should get uid/gid fro elsewhere as they're probably wrong locally
502$cbp->{"cb.full"}->{$type}->{$targetname}->{'uid'} = $uid;
503$cbp->{"cb.full"}->{$type}->{$targetname}->{'gid'} = $gid;
504$cbp->{"cb.full"}->{$type}->{$targetname}->{'mode'} = sprintf("%04o",$mode & 07777);
505$cbp->{"cb.full"}->{$type}->{$targetname}->{'mtime'} = $mtime;
506pb_log(2,"Adding $File::Find::name ($uid,$gid,$mode) to cbp\n");
[1487]507}
Note: See TracBrowser for help on using the repository browser.