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

Last change on this file since 1662 was 1662, checked in by Bruno Cornec, 12 years ago
  • Lots of modificiation to have cb deliver common content first
  • Property svn:executable set to *
File size: 14.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;
86#use Cwd 'realpath';
[1662]87use Carp qw/confess cluck/;
[1485]88use File::Find;
[1487]89use Archive::Tar;
[1485]90use Getopt::Long;
91use Pod::Usage;
92use Data::Dumper;
[1490]93use Time::Local;
94use Net::SSH2;
[1485]95use ProjectBuilder::Base;
96use ProjectBuilder::Conf;
97use ProjectBuilder::VCS;
98use DBI;
99use DBD::SQLite;
[1466]100
[1485]101# settings
102my $debug = 0;
103my $help = undef;
104my $man = undef;
105my $source = undef;
106my $machine = undef;
107my $plugin = undef;
108my $quiet = undef;
109my $force = undef;
110my $log = undef;
111my $LOG = undef;
[1662]112my $findtarget = undef;
[1466]113
[1485]114my ($cbver,$cbrev) = cb_version_init();
115my $appname = "cb";
116$ENV{'PBPROJ'} = $appname;
[1466]117
118# Initialize the syntax string
[1485]119pb_syntax_init("$appname (aka CasparBuster) Version $cbver-$cbrev\n");
[1466]120
[1485]121# parse command-line options
122GetOptions(
123 'machine|m=s' => \$machine,
124 'debug|d+' => \$debug,
125 'help|h' => \$help,
126 'quiet|q' => \$quiet,
127 'force|f' => \$force,
128 'man' => \$man,
129 'logfile|l=s' => \$log,
130 'source|s=s' => \$source,
131 'plugin|p=s' => \$plugin,
[1466]132) || pb_syntax(-1,0);
133
[1485]134if (defined $help) {
[1466]135 pb_syntax(0,1);
136}
[1485]137if (defined $man) {
[1466]138 pb_syntax(0,2);
139}
[1485]140if (defined $quiet) {
141 $debug=-1;
[1466]142}
[1485]143if (defined $log) {
144 open(LOG,"> $log") || die "Unable to log to $log: $!";
145 $LOG = \*LOG;
146 $debug = 0 if ($debug == -1);
[1466]147}
148
[1485]149pb_log_init($debug, $LOG);
[1662]150pb_temp_init();
151pb_log(1,"Starting cb\n");
[1466]152
[1485]153# Get conf file in context
154pb_conf_init($appname);
155# The personal one if there is such
156pb_conf_add("$ENV{'HOME'}/.cbrc") if (-f "$ENV{'HOME'}/.cbrc");
157# The system one
158pb_conf_add(cb_env_conffile());
[1466]159
[1485]160# Get configuration parameters
161my %cb;
[1489]162my $cbp = ();
[1485]163my $cb = \%cb;
[1662]164($cb->{'basedir'},$cb->{'cms'},$cb->{'database'}) = pb_conf_get("cbbasedir","cbcms","cbdatabase");
[1485]165pb_log(2,"%cb: ",Dumper($cb));
[1466]166
[1485]167if (defined $plugin) {
168 # Load plugins
169 cb_plugin_load();
[1466]170}
171
[1485]172my $basedir = $cb->{'basedir'}->{$appname};
173eval { $basedir =~ s/(\$ENV.+\})/$1/eeg };
[1466]174
[1485]175# Create basedir if it doesn't exist
176die "Unable to find base directory at $basedir" if (not -d $basedir);
[1466]177
[1487]178pb_log(1, "DEBUG MODE, not doing anything, just printing\nDEBUG: basedir = $basedir\n");
[1466]179
[1487]180# Create database if not existing and give a handler
181my $db = "$basedir/$cb->{'database'}->{$appname}";
[1466]182
[1485]183my $precmd = "";
184if (! -f $db) {
[1487]185 $precmd = "CREATE TABLE dates (id INTEGER PRIMARY KEY AUTOINCREMENT, date INTEGER, file VARCHAR[65535], machine VARCHAR[65535])";
[1466]186}
187
[1485]188my $dbh = DBI->connect("dbi:SQLite:dbname=$db","","",
189 { RaiseError => 1, AutoCommit => 1 })
190 || die "Unable to connect to $db";
[1490]191my $sth;
[1466]192
[1485]193if ($precmd ne "") {
[1490]194 $sth = $dbh->prepare(qq{$precmd}) || die "Unable to create table into $db";
[1485]195 if ($debug) {
[1487]196 pb_log(1,"DEBUG: Creating DB $db\n");
197 pb_log(1,"DEBUG: with command $precmd\n");
[1466]198 } else {
[1485]199 $sth->execute();
[1466]200 }
[1487]201 $sth->finish();
[1466]202}
203
[1487]204# Define destination dir and populate with a VCS export
[1662]205my $dest = "$ENV{'PBTMP'}/vcs.$$";
[1487]206my $scheme = $cb->{'cms'}->{$appname};
[1662]207
208# Avoids too many permission changes
209umask(0022);
[1487]210pb_vcs_export(pb_vcs_get_uri($scheme,$basedir),$basedir,$dest);
[1466]211
[1487]212# Now distribute to the right machines
[1489]213if (defined $machine) {
[1487]214 cb_distribute($machine);
[1485]215} else {
[1662]216 # Distribute to all
217 # First dir level is the machine, then the content
218 opendir(DIR,$dest) || die "Unable to open $dest: $!";
219 foreach my $m (readdir(DIR)) {
220 next if ($m =~ /^\./);
221 next if (! -d $m);
222 # Machine name
223 cb_distribute($m);
224 closedir(DIR);
[1466]225 }
226}
227
[1487]228# Cleanup
229$dbh->disconnect;
[1662]230pb_exit();
[1487]231
232# End of Main
233
234# Distribute files to target machines
235sub cb_distribute {
236
237my $machine = shift;
238
239pb_log(2,"Entering into cb_distribute with machine $machine\n");
[1662]240confess "No machine fiven to cb_distribute" if (not defined $machine);
[1487]241
242# Use potentially a remote account if defined
[1490]243my $remote = undef;
[1662]244my ($account) = pb_conf_get_if("cbaccount");
245$remote = $account->{$machine} if ((defined $account) && (defined $account->{$machine}));
[1490]246pb_log(1, "DEBUG: remote account1 = $remote\n") if (defined $remote);
247$remote = getpwuid($<) if (not defined $remote);
248pb_log(1, "DEBUG: remote account2 = $remote\n");
[1487]249
250# Now handle plugins if any
251if (defined $plugin) {
252 foreach my $p (split(/,/,$plugin)) {
253 pb_log(1,"Getting context for plugin $p\n");
254 $cbp = cb_plugin_get($p,$cbp);
255 # Adds mtime info to the plugin structure
[1494]256 foreach my $type ('files','dirs','dirsandfiles') {
[1487]257 foreach my $f (keys %{$cbp->{$p}->{$type}}) {
[1662]258 my $tdir = "$dest/$machine";
[1487]259 if (-r "$tdir/$f") {
260 my ($dev,$ino,$mode,$nlink,$uid,$gid,$rdev,$size,$atime,$mtime,$ctime,$blksize,$blocks) = stat("$tdir/$f") || die "Unable to stat $tdir: $!";
261 $cbp->{$p}->{$type}->{$f}->{'mtime'} = $mtime;
262 } else {
263 pb_log(0,"WARNING: Unable to read $tdir/$f from plugin $p\n");
264 }
265 }
266 }
267 }
[1466]268}
269
[1487]270# Handle this source
271if (defined $source) {
272 my $fullsource = "$source";
[1662]273 $fullsource = "$machine/$source";
[1487]274 pb_log(2,"fullsource is $fullsource\n");
275 my ($dev,$ino,$mode,$nlink,$uid,$gid,$rdev,$size,$atime,$mtime,$ctime,$blksize,$blocks) = stat("$dest/$fullsource") || die "Unable to stat $fullsource: $!";
276 my $type = 'files';
277 if (-d $fullsource) {
[1494]278 $type = 'dirsandfiles';
[1487]279 }
280 die "ERROR: Only able to handle files or dirs with option --source\n" if ((! -d $fullsource) && (! -f $fullsource));
[1466]281
[1662]282 # We should get uid/gid fro elsewhere as they're probably wrong locally
[1487]283 $cbp->{"cb.source"}->{$type}->{$source}->{'uid'} = $uid;
284 $cbp->{"cb.source"}->{$type}->{$source}->{'gid'} = $gid;
285 $cbp->{"cb.source"}->{$type}->{$source}->{'mode'} = $mode;
286 $cbp->{"cb.source"}->{$type}->{$source}->{'mtime'} = $mtime;
287}
[1466]288
[1662]289($cb->{'commondir'},$cb->{'websrv'},$cb->{'webdir'}) = pb_conf_get_if("cbcommondir","cbwebsrv","cbwebdir");
290
[1487]291if ((not defined $source) && (not defined $plugin)) {
292 # Here we need to take all content under $dest considering that machine
[1662]293 $findtarget = "$dest/$machine";
294 find(\&cb_add_to_cbp,($findtarget));
[1466]295}
[1662]296pb_log(1,"INFO: RAW cbp: ".Dumper(%$cbp)."\n");
[1466]297
[1487]298# Clean up cbp structure by comparing with data stored in the DB
299# Only keep the more recent modified content
[1490]300# Allow for errors to occur at DBI level
301$dbh->{RaiseError} = 0;
302my $checkdb = 1;
[1662]303my $dbcmd = "SELECT id,date,file,machine FROM dates WHERE machine=\"$machine\"";
[1490]304if (! ($sth = $dbh->prepare(qq{$dbcmd}))) {
305 pb_log(0,"Unable to prepare DB statement $dbcmd\n");
306 $checkdb = 0;
307}
308# DisAllow for errors to occur at DBI level
309$dbh->{RaiseError} = 1;
310my $dbid = ();
311if ($checkdb == 1) {
312 $sth->execute();
[1662]313 # Check what in cbp is in the DB and deploy only if necessary or forced
[1490]314 foreach my $k (keys %{$cbp}) {
[1494]315 foreach my $type ('files','dirs','dirsandfiles') {
[1490]316 foreach my $o (keys %{$cbp->{$k}->{$type}}) {
317 # Compare with info from DB
318 foreach my $row ($sth->fetch) {
319 next if (not defined $row);
320 my ($id, $date, $file, $mac1) = @$row;
321 # If less recent than in the DB remove it
322 $cbp->{$k}->{$type}->{$o}->{'deleted'} = "true" if ((defined $file) && ($file eq $o) && ($date > $cbp->{$k}->{$type}->{$o}->{'mtime'}));
323 $dbid->{$o} = $id;
324 }
[1487]325 }
326 }
327 }
[1490]328 $sth->finish();
[1487]329}
330pb_log(2,"INFO: cleaned cbp: ".Dumper($cbp)."\n");
[1466]331
[1487]332# Now create a tar containing all the relevant content
[1490]333# We need to loop separately to allow for DB to not exist in the previous loop !
334my $tdir = undef;
[1662]335$tdir = "$dest/$machine";
[1490]336chdir("$tdir") || die "ERROR: Unable to chdir to $tdir\n";
337pb_log(2,"Working now under $tdir\n");
338
[1487]339my $tar = Archive::Tar->new;
[1490]340$tar->setcwd($tdir);
341my $curdate = time();
[1489]342foreach my $k (keys %{$cbp}) {
[1494]343 foreach my $type ('files','dirs','dirsandfiles') {
344 # TODO: for dirs we may remove the files below ?
[1490]345 foreach my $o ((keys %{$cbp->{$k}->{$type}})) {
[1662]346 if ((defined $force) || (not defined $cbp->{$k}->{$type}->{$o}->{'deleted'})) {
347 $tar->add_files("$o");
[1490]348 # Add an entry to the DB
349 if (defined $dbid->{$o}) {
350 # Modify an existing entry
[1662]351 $dbcmd = "UPDATE dates SET date=\"$curdate\",file=\"$o\" WHERE id=\"$dbid->{$o}\"";
[1490]352 if (not $debug) {
353 $sth = $dbh->prepare(qq{$dbcmd});
[1662]354 $sth->execute();
[1490]355 }
[1662]356 pb_log(0,"Executing in DB: $dbcmd with curdate=$curdate,file=$o,id=$dbid->{$o}\n");
[1490]357 } else {
358 # Add an new entry
[1662]359 $dbcmd = "INSERT INTO dates VALUES (NULL,?,?,\"$machine\")";
[1490]360 if (not $debug) {
361 $sth = $dbh->prepare(qq{$dbcmd});
[1662]362 $sth->execute($curdate,$o);
[1490]363 }
[1662]364 pb_log(0,"Executing in DB: $dbcmd with curdate=$curdate,file=$o,machine=$machine\n");
[1490]365 }
366 if (not $debug) {
367 $sth->finish();
368 }
369 }
370 }
371 }
[1489]372}
[1662]373my $tarfile = "$ENV{'PBTMP'}/cbcontent$$.tar";
[1489]374$tar->write("$tarfile");
[1466]375
[1490]376my $ssh2;
377my $chan;
378
379# deal with content first
[1662]380# Create remote connection and copy tar file there
381$ssh2 = Net::SSH2->new();
382$ssh2->debug(1) if ($debug > 2);
383
384my $mach = $machine;
385if ((defined $cb->{'commondir'}) && (defined $cb->{'commondir'}->{$machine})) {
386 confess "Please provide a cbwebsrv config parameter in order to use common delivery" if ((not defined $cb->{'websrv'}) && (not defined $cb->{'websrv'}->{$machine}));
387 $mach = $cb->{'websrv'}->{$machine};
[1490]388}
[1662]389$ssh2->connect($mach);
390my $hdir = (getpwnam(getpwuid($<)))[7];
391if ($ssh2->auth_publickey($remote,"$hdir/.ssh/id_dsa.pub","$hdir/.ssh/id_dsa")) {
392 $chan = $ssh2->channel();
393 $chan->exec("mkdir -p $ENV{'PBTMP'}");
394 $chan->close;
[1490]395
[1662]396 $ssh2->scp_put($tarfile,$tarfile);
397 pb_log(0,"INFO: Created $ENV{'PBTMP'} on $remote\@$mach and copying content\n");
[1466]398} else {
[1662]399 pb_log(0,"ERROR: Unable to authenticate to $remote\@$mach\n");
400 return;
[1466]401}
402
[1662]403my $path = "/";
404my $tbextract = "";
405if ((defined $cb->{'commondir'}) && (defined $cb->{'commondir'}->{$machine})) {
406 $path = $cb->{'webdir'}->{$machine};
407 $tbextract = $cb->{'commondir'}->{$machine};
408}
409$chan = $ssh2->channel();
410$chan->shell();
[1490]411if (not $debug) {
[1662]412 print $chan "sudo tar -C $path -x -f $tarfile $tbextract\n";
413 pb_log(0,"WARNING: $_\n") while (<$chan>);
[1466]414} else {
[1662]415 print $chan "tar -C $path -t -f $tarfile $tbextract\n";
416 pb_log(2,"INFO: tar content: $_") while (<$chan>);
[1466]417}
[1490]418
[1662]419pb_log(0,"INFO: Extracting $tbextract (on $machine) $tarfile under $path\n");
420
[1490]421foreach my $k (keys %{$cbp}) {
[1494]422 foreach my $type ('files','dirs','dirsandfiles') {
423 # TODO: do we act recursively for dirsandfiles at least for uid/gid ?
[1490]424 foreach my $o ((keys %{$cbp->{$k}->{$type}})) {
[1662]425 # Note that $path/$o is remote only
426 if ((defined $force) || (not defined $cbp->{$k}->{$type}->{$o}->{'deleted'})) {
[1490]427 if ($debug) {
[1662]428 pb_log(1,"INFO: Executing (on $machine) sudo chown $cbp->{$k}->{$type}->{$o}->{'uid'}:$cbp->{$k}->{$type}->{$o}->{'gid'} $path/$o\n");
429 pb_log(1,"INFO: Executing (on $machine) sudo chmod $cbp->{$k}->{$type}->{$o}->{'mode'} $path/$o\n");
[1490]430 } else {
[1662]431 # TODO: remove hardcoded commands
432 print $chan "sudo chown $cbp->{$k}->{$type}->{$o}->{'uid'}:$cbp->{$k}->{$type}->{$o}->{'gid'} $path/$o\n";
433 print $chan "sudo chmod $cbp->{$k}->{$type}->{$o}->{'mode'} $path/$o\n";
[1490]434 }
[1662]435 pb_log(0,"INFO: Delivering $path/$o on $machine\n");
[1490]436 }
437 }
438 }
439 if (defined $cbp->{$k}->{'reloadscript'}) {
[1662]440 if (not $debug) {
441 print $chan "$cbp->{$k}->{'reloadscript'}\n";
[1490]442 }
[1662]443 pb_log(0,"INFO: Executing (on $machine) $cbp->{$k}->{'reloadscript'}\n");
[1490]444 }
445}
446
[1662]447# Remote cleanup
[1490]448if (not $debug) {
[1662]449 print $chan "rm -rf $ENV{'PBTMP'}\n";
[1490]450} else {
[1662]451 pb_log(1,"Remove remote directory $ENV{'PBTMP'} on $machine\n");
[1490]452}
[1662]453$chan->close();
454$ssh2->disconnect();
455
456chdir("/");
[1487]457pb_log(2,"Exiting cb_distribute\n");
[1466]458}
[1487]459
460sub cb_add_to_cbp {
461
[1662]462pb_log(3,"Entering into cb_add_to_cbp\n");
[1487]463my $type = 'files';
464if (-d $File::Find::name) {
465 $type = 'dirs';
466}
467
[1662]468# Target name is without the $findtarget part
469my $targetname = $File::Find::name;
470$targetname =~ s|^$findtarget[/]*||;
471
472return if ($targetname eq "");
473
474my ($dev,$ino,$mode,$nlink,$uid,$gid,$rdev,$size,$atime,$mtime,$ctime,$blksize,$blocks) = stat($File::Find::name);
475die "Unable to stat $File::Find::name" if (not defined $mode);
476# We should get uid/gid fro elsewhere as they're probably wrong locally
477$cbp->{"cb.full"}->{$type}->{$targetname}->{'uid'} = $uid;
478$cbp->{"cb.full"}->{$type}->{$targetname}->{'gid'} = $gid;
479$cbp->{"cb.full"}->{$type}->{$targetname}->{'mode'} = sprintf("%04o",$mode & 07777);
480$cbp->{"cb.full"}->{$type}->{$targetname}->{'mtime'} = $mtime;
481pb_log(2,"Adding $File::Find::name ($uid,$gid,$mode) to cbp\n");
[1487]482}
Note: See TracBrowser for help on using the repository browser.