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

Last change on this file was 1765, checked in by Bruno Cornec, 11 years ago
  • Adds support for Debian 7.0 to some projects
  • latest cb commits
  • mindi 2.1.5 announce
  • 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]168my $basedir = $cb->{'basedir'}->{$appname};
169eval { $basedir =~ s/(\$ENV.+\})/$1/eeg };
[1466]170
[1485]171# Create basedir if it doesn't exist
172die "Unable to find base directory at $basedir" if (not -d $basedir);
[1466]173
[1487]174pb_log(1, "DEBUG MODE, not doing anything, just printing\nDEBUG: basedir = $basedir\n");
[1466]175
[1487]176# Create database if not existing and give a handler
177my $db = "$basedir/$cb->{'database'}->{$appname}";
[1466]178
[1485]179my $precmd = "";
180if (! -f $db) {
[1765]181 $precmd = "CREATE TABLE dates (id INTEGER PRIMARY KEY AUTOINCREMENT, date INTEGER, file VARCHAR[65535], machine VARCHAR[65535], mode VARCHAR[4], uid VARCHAR[5], gid VARCHAR[5])";
[1466]182}
183
[1485]184my $dbh = DBI->connect("dbi:SQLite:dbname=$db","","",
185 { RaiseError => 1, AutoCommit => 1 })
186 || die "Unable to connect to $db";
[1490]187my $sth;
[1466]188
[1485]189if ($precmd ne "") {
[1490]190 $sth = $dbh->prepare(qq{$precmd}) || die "Unable to create table into $db";
[1485]191 if ($debug) {
[1487]192 pb_log(1,"DEBUG: Creating DB $db\n");
193 pb_log(1,"DEBUG: with command $precmd\n");
[1466]194 } else {
[1485]195 $sth->execute();
[1466]196 }
[1487]197 $sth->finish();
[1466]198}
199
[1487]200# Define destination dir and populate with a VCS export
[1662]201my $dest = "$ENV{'PBTMP'}/vcs.$$";
[1487]202my $scheme = $cb->{'cms'}->{$appname};
[1662]203
204# Avoids too many permission changes
205umask(0022);
[1487]206pb_vcs_export(pb_vcs_get_uri($scheme,$basedir),$basedir,$dest);
[1466]207
[1765]208# Load all plugins plus the additional manually defined
209cb_plugin_load();
210if (defined $plugin) {
211 pb_conf_add("$basedir/plugins/$plugin") if (-f "$basedir/plugins/$plugin");
212}
213
[1487]214# Now distribute to the right machines
[1489]215if (defined $machine) {
[1487]216 cb_distribute($machine);
[1485]217} else {
[1662]218 # Distribute to all
219 # First dir level is the machine, then the content
220 opendir(DIR,$dest) || die "Unable to open $dest: $!";
221 foreach my $m (readdir(DIR)) {
222 next if ($m =~ /^\./);
223 next if (! -d $m);
224 # Machine name
225 cb_distribute($m);
226 closedir(DIR);
[1466]227 }
228}
229
[1487]230# Cleanup
231$dbh->disconnect;
[1662]232pb_exit();
[1487]233
234# End of Main
235
236# Distribute files to target machines
237sub cb_distribute {
238
239my $machine = shift;
240
241pb_log(2,"Entering into cb_distribute with machine $machine\n");
[1765]242confess "No machine given to cb_distribute" if (not defined $machine);
[1487]243
244# Use potentially a remote account if defined
[1490]245my $remote = undef;
[1662]246my ($account) = pb_conf_get_if("cbaccount");
247$remote = $account->{$machine} if ((defined $account) && (defined $account->{$machine}));
[1490]248pb_log(1, "DEBUG: remote account1 = $remote\n") if (defined $remote);
249$remote = getpwuid($<) if (not defined $remote);
250pb_log(1, "DEBUG: remote account2 = $remote\n");
[1487]251
252# Now handle plugins if any
253if (defined $plugin) {
254 foreach my $p (split(/,/,$plugin)) {
255 pb_log(1,"Getting context for plugin $p\n");
256 $cbp = cb_plugin_get($p,$cbp);
257 # Adds mtime info to the plugin structure
[1494]258 foreach my $type ('files','dirs','dirsandfiles') {
[1487]259 foreach my $f (keys %{$cbp->{$p}->{$type}}) {
[1662]260 my $tdir = "$dest/$machine";
[1487]261 if (-r "$tdir/$f") {
262 my ($dev,$ino,$mode,$nlink,$uid,$gid,$rdev,$size,$atime,$mtime,$ctime,$blksize,$blocks) = stat("$tdir/$f") || die "Unable to stat $tdir: $!";
263 $cbp->{$p}->{$type}->{$f}->{'mtime'} = $mtime;
264 } else {
265 pb_log(0,"WARNING: Unable to read $tdir/$f from plugin $p\n");
266 }
267 }
268 }
269 }
[1466]270}
271
[1487]272# Handle this source
273if (defined $source) {
274 my $fullsource = "$source";
[1662]275 $fullsource = "$machine/$source";
[1487]276 pb_log(2,"fullsource is $fullsource\n");
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
[1765]283 cb_fill_cbp("cb.source","$dest/$fullsource",$type,$source)
[1487]284}
[1466]285
[1662]286($cb->{'commondir'},$cb->{'websrv'},$cb->{'webdir'}) = pb_conf_get_if("cbcommondir","cbwebsrv","cbwebdir");
287
[1487]288if ((not defined $source) && (not defined $plugin)) {
289 # Here we need to take all content under $dest considering that machine
[1662]290 $findtarget = "$dest/$machine";
291 find(\&cb_add_to_cbp,($findtarget));
[1679]292 # And we also need all what is common, but not what is for the web side
293 foreach my $c (keys $cb->{'commondir'}) {
294 $findtarget = "$dest/$c";
295 opendir(DIR,"$findtarget") || die "Unable to open $dest/$c: $!";
296 foreach my $m (readdir(DIR)) {
297 next if ($m =~ /^\./);
298 next if ($m eq $cb->{'commondir'}->{$c});
299 find(\&cb_add_to_cbp,("$findtarget/$m"));
300 }
301 closedir(DIR);
302 }
[1466]303}
[1662]304pb_log(1,"INFO: RAW cbp: ".Dumper(%$cbp)."\n");
[1466]305
[1487]306# Clean up cbp structure by comparing with data stored in the DB
307# Only keep the more recent modified content
[1490]308# Allow for errors to occur at DBI level
309$dbh->{RaiseError} = 0;
310my $checkdb = 1;
[1662]311my $dbcmd = "SELECT id,date,file,machine FROM dates WHERE machine=\"$machine\"";
[1490]312if (! ($sth = $dbh->prepare(qq{$dbcmd}))) {
313 pb_log(0,"Unable to prepare DB statement $dbcmd\n");
314 $checkdb = 0;
315}
316# DisAllow for errors to occur at DBI level
317$dbh->{RaiseError} = 1;
318my $dbid = ();
319if ($checkdb == 1) {
320 $sth->execute();
[1662]321 # Check what in cbp is in the DB and deploy only if necessary or forced
[1490]322 foreach my $k (keys %{$cbp}) {
[1494]323 foreach my $type ('files','dirs','dirsandfiles') {
[1490]324 foreach my $o (keys %{$cbp->{$k}->{$type}}) {
325 # Compare with info from DB
326 foreach my $row ($sth->fetch) {
327 next if (not defined $row);
328 my ($id, $date, $file, $mac1) = @$row;
329 # If less recent than in the DB remove it
330 $cbp->{$k}->{$type}->{$o}->{'deleted'} = "true" if ((defined $file) && ($file eq $o) && ($date > $cbp->{$k}->{$type}->{$o}->{'mtime'}));
331 $dbid->{$o} = $id;
332 }
[1487]333 }
334 }
335 }
[1490]336 $sth->finish();
[1487]337}
338pb_log(2,"INFO: cleaned cbp: ".Dumper($cbp)."\n");
[1466]339
[1487]340# Now create a tar containing all the relevant content
[1490]341# We need to loop separately to allow for DB to not exist in the previous loop !
342my $tdir = undef;
[1662]343$tdir = "$dest/$machine";
[1490]344chdir("$tdir") || die "ERROR: Unable to chdir to $tdir\n";
345pb_log(2,"Working now under $tdir\n");
346
[1487]347my $tar = Archive::Tar->new;
[1490]348my $curdate = time();
[1489]349foreach my $k (keys %{$cbp}) {
[1494]350 foreach my $type ('files','dirs','dirsandfiles') {
351 # TODO: for dirs we may remove the files below ?
[1490]352 foreach my $o ((keys %{$cbp->{$k}->{$type}})) {
[1662]353 if ((defined $force) || (not defined $cbp->{$k}->{$type}->{$o}->{'deleted'})) {
[1683]354 if ( -r "$tdir/$o" ) {
355 pb_log(1,"INFO: Adding to the tar file $tdir/$o\n");
356 chdir($tdir);
357 $tar->add_files("$o");
358 } else {
359 # It's in the common place instead
360 foreach my $c (keys $cb->{'commondir'}) {
361 if (-r "$dest/$c/$o") {
362 pb_log(1,"INFO: Adding to the tar file $dest/$c/$o\n");
363 chdir("$dest/$c");
364 $tar->add_files("$o");
365 }
366 }
367 }
[1490]368 # Add an entry to the DB
369 if (defined $dbid->{$o}) {
370 # Modify an existing entry
[1662]371 $dbcmd = "UPDATE dates SET date=\"$curdate\",file=\"$o\" WHERE id=\"$dbid->{$o}\"";
[1490]372 if (not $debug) {
373 $sth = $dbh->prepare(qq{$dbcmd});
[1662]374 $sth->execute();
[1490]375 }
[1662]376 pb_log(0,"Executing in DB: $dbcmd with curdate=$curdate,file=$o,id=$dbid->{$o}\n");
[1490]377 } else {
378 # Add an new entry
[1662]379 $dbcmd = "INSERT INTO dates VALUES (NULL,?,?,\"$machine\")";
[1490]380 if (not $debug) {
381 $sth = $dbh->prepare(qq{$dbcmd});
[1662]382 $sth->execute($curdate,$o);
[1490]383 }
[1662]384 pb_log(0,"Executing in DB: $dbcmd with curdate=$curdate,file=$o,machine=$machine\n");
[1490]385 }
386 if (not $debug) {
387 $sth->finish();
388 }
389 }
390 }
391 }
[1489]392}
[1662]393my $tarfile = "$ENV{'PBTMP'}/cbcontent$$.tar";
[1489]394$tar->write("$tarfile");
[1466]395
[1490]396my $ssh2;
397my $chan;
398
[1662]399my $mach = $machine;
400if ((defined $cb->{'commondir'}) && (defined $cb->{'commondir'}->{$machine})) {
401 confess "Please provide a cbwebsrv config parameter in order to use common delivery" if ((not defined $cb->{'websrv'}) && (not defined $cb->{'websrv'}->{$machine}));
402 $mach = $cb->{'websrv'}->{$machine};
[1490]403}
404
[1682]405$ssh2 = cb_ssh_init($remote,$mach,$debug);
[1466]406
[1683]407if (!($ssh2->scp_put($tarfile,$tarfile))) {
408 my @error = $ssh2->error();
409 print "@error\n";
410 confess "Unable to copy tar file $tarfile to $mach\n";
411}
[1667]412pb_log(0,"INFO: Copying content under $ENV{'PBTMP'} on $remote\@$mach\n");
413
[1662]414my $path = "/";
415my $tbextract = "";
416if ((defined $cb->{'commondir'}) && (defined $cb->{'commondir'}->{$machine})) {
417 $path = $cb->{'webdir'}->{$machine};
[1683]418 #$tbextract = $cb->{'commondir'}->{$machine};
[1662]419}
420$chan = $ssh2->channel();
[1667]421confess "Unable to launch remote shell through Net:SSH2 ($remote\@$mach)" if (not $chan->shell());
422
[1490]423if (not $debug) {
[1683]424 # Reminder: sudo should be configured for this account as Defaults !requiretty for this to work
[1765]425 print $chan "sudo tar -C $path --no-overwrite-dir -x -f $tarfile $tbextract\n";
[1662]426 pb_log(0,"WARNING: $_\n") while (<$chan>);
[1466]427} else {
[1662]428 print $chan "tar -C $path -t -f $tarfile $tbextract\n";
429 pb_log(2,"INFO: tar content: $_") while (<$chan>);
[1466]430}
[1490]431
[1682]432pb_log(0,"INFO: Extracting $tbextract (on $mach) $tarfile under $path\n");
[1662]433
[1490]434foreach my $k (keys %{$cbp}) {
[1494]435 foreach my $type ('files','dirs','dirsandfiles') {
436 # TODO: do we act recursively for dirsandfiles at least for uid/gid ?
[1490]437 foreach my $o ((keys %{$cbp->{$k}->{$type}})) {
[1662]438 # Note that $path/$o is remote only
439 if ((defined $force) || (not defined $cbp->{$k}->{$type}->{$o}->{'deleted'})) {
[1490]440 if ($debug) {
[1682]441 #pb_log(1,"INFO: Executing (on $mach) sudo chown $cbp->{$k}->{$type}->{$o}->{'uid'}:$cbp->{$k}->{$type}->{$o}->{'gid'} $path/$o\n");
442 #pb_log(1,"INFO: Executing (on $mach) sudo chmod $cbp->{$k}->{$type}->{$o}->{'mode'} $path/$o\n");
[1490]443 } else {
[1662]444 # TODO: remove hardcoded commands
[1679]445 #print $chan "sudo chown $cbp->{$k}->{$type}->{$o}->{'uid'}:$cbp->{$k}->{$type}->{$o}->{'gid'} $path/$o\n";
[1664]446 # TODO: get a correct mode before setting it up
447 #print $chan "sudo chmod $cbp->{$k}->{$type}->{$o}->{'mode'} $path/$o\n";
[1490]448 }
[1682]449 pb_log(0,"INFO: Delivering $path/$o on $mach\n");
[1490]450 }
451 }
452 }
453 if (defined $cbp->{$k}->{'reloadscript'}) {
[1662]454 if (not $debug) {
[1683]455 print $chan "sudo $cbp->{$k}->{'reloadscript'}\n";
[1490]456 }
[1683]457 pb_log(0,"INFO: Executing (on $mach) $cbp->{$k}->{'reloadscript'} as root\n");
[1490]458 }
459}
460
[1683]461pb_log(0,"INFO: Executing (on $mach) /usr/local/bin/mk$mach if present as root\n");
462if (not $debug) {
463 # Using Net::SSH2 here was not working (due to the shell ?)
464 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");
465}
466
[1662]467# Remote cleanup
[1490]468if (not $debug) {
[1662]469 print $chan "rm -rf $ENV{'PBTMP'}\n";
[1490]470} else {
[1683]471 pb_log(1,"INFO: Please remove remote directory $ENV{'PBTMP'} on $mach\n");
[1490]472}
[1662]473$chan->close();
474
[1667]475cb_ssh_close($ssh2);
476
[1662]477chdir("/");
[1487]478pb_log(2,"Exiting cb_distribute\n");
[1466]479}
[1487]480
481sub cb_add_to_cbp {
482
[1662]483pb_log(3,"Entering into cb_add_to_cbp\n");
[1487]484my $type = 'files';
485if (-d $File::Find::name) {
486 $type = 'dirs';
487}
488
[1662]489# Target name is without the $findtarget part
490my $targetname = $File::Find::name;
491$targetname =~ s|^$findtarget[/]*||;
492
493return if ($targetname eq "");
494
[1765]495cb_fill_cbp("cb.full",$File::Find::name,$type,$targetname)
[1487]496}
[1765]497
498sub cb_fill_cbp {
499
500my $k = shift;
501my $f = shift;
502my $type = shift;
503my $targetname = shift;
504
505my ($dev,$ino,$mode,$nlink,$uid,$gid,$rdev,$size,$atime,$mtime,$ctime,$blksize,$blocks) = stat($f);
506($dev,$ino,$mode,$nlink,$uid,$gid,$rdev,$size,$atime,$mtime,$ctime,$blksize,$blocks) = lstat($f) if (not defined $mode);
507die "Unable to stat $f" if (not defined $mode);
508# We should get uid/gid from elsewhere as they're probably wrong locally
509$cbp->{$k}->{$type}->{$targetname}->{'uid'} = $uid;
510$cbp->{$k}->{$type}->{$targetname}->{'gid'} = $gid;
511$cbp->{$k}->{$type}->{$targetname}->{'mode'} = sprintf("%04o",$mode & 07777);
512$cbp->{$k}->{$type}->{$targetname}->{'mtime'} = $mtime;
513pb_log(2,"Adding $f ($uid,$gid,$mode) to cbp\n");
514}
Note: See TracBrowser for help on using the repository browser.