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

Last change on this file since 1494 was 1494, checked in by Bruno Cornec, 12 years ago
  • Add support for cbplugindirsandfiles. This param provides support for direstories and all their children, whereas cbplugindirs is restricted to the directory alone. Usefull for named support
  • Add named plugin
  • cbusterize wworks now mostly for named (access to root owned file is still aproblem to solve)
  • cb also supports the new cbplugindirsandfiles param
  • Property svn:executable set to *
File size: 13.8 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
[1466]42
[1485]43=item B<--source> I<path>
[1466]44
[1485]45Specify the path of the source file or directory to deploy with CasparBuster. Multiple paths can be specified separated by ','.
[1466]46
[1485]47=item B<--plugin> I<name>
[1466]48
[1485]49Specify the name of the plugin to deploy with CasparBuster. Multiple plugins can be specified separated by ','.
50A 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]51
[1485]52=back
[1466]53
[1485]54=head1 DESCRIPTION
[1466]55
[1485]56Deploy 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]57
[1485]58=head1 EXAMPLES
[1466]59
[1485]60 # this will deploy the appropriate CasparBuster environment for DHCP
61 # from the base ~/prj/musique-ancienne.org directory (Cf cbbasedir in cb.conf)
62 # containing the directory victoria2 for this machine
63 # to which it will copy the required files
[1466]64
[1485]65 cb -m victoria2 -p dhcpd
[1466]66
[1485]67=head1 AUTHOR
[1466]68
69=over 4
70
[1485]71Bruno Cornec, http://brunocornec.wordpress.com
[1466]72
[1485]73=back
[1466]74
[1485]75=head1 LICENSE
[1466]76
[1485]77Copyright (C) 2012 Bruno Cornec <bruno@project-builder.org>
78Released under the GPLv2 or the Artistic license at your will.
[1466]79
80=cut
[1485]81use strict;
82use CasparBuster::Version;
83use CasparBuster::Env;
84use CasparBuster::Plugin;
85#use Cwd 'realpath';
86use File::Find;
[1487]87use Archive::Tar;
[1485]88use Getopt::Long;
89use Pod::Usage;
90use Data::Dumper;
[1490]91use Time::Local;
92use Net::SSH2;
[1485]93use ProjectBuilder::Base;
94use ProjectBuilder::Conf;
95use ProjectBuilder::VCS;
96use DBI;
97use DBD::SQLite;
[1466]98
[1485]99# settings
100my $debug = 0;
101my $help = undef;
102my $man = undef;
103my $source = undef;
104my $machine = undef;
105my $plugin = undef;
106my $quiet = undef;
107my $force = undef;
108my $log = undef;
109my $LOG = undef;
[1466]110
[1485]111my ($cbver,$cbrev) = cb_version_init();
112my $appname = "cb";
113$ENV{'PBPROJ'} = $appname;
114pb_temp_init();
[1466]115
116# Initialize the syntax string
[1485]117pb_syntax_init("$appname (aka CasparBuster) Version $cbver-$cbrev\n");
[1466]118
[1485]119# parse command-line options
120GetOptions(
121 'machine|m=s' => \$machine,
122 'debug|d+' => \$debug,
123 'help|h' => \$help,
124 'quiet|q' => \$quiet,
125 'force|f' => \$force,
126 'man' => \$man,
127 'logfile|l=s' => \$log,
128 'source|s=s' => \$source,
129 'plugin|p=s' => \$plugin,
[1466]130) || pb_syntax(-1,0);
131
[1485]132if (defined $help) {
[1466]133 pb_syntax(0,1);
134}
[1485]135if (defined $man) {
[1466]136 pb_syntax(0,2);
137}
[1485]138if (defined $quiet) {
139 $debug=-1;
[1466]140}
[1485]141if (defined $log) {
142 open(LOG,"> $log") || die "Unable to log to $log: $!";
143 $LOG = \*LOG;
144 $debug = 0 if ($debug == -1);
[1466]145}
146
[1485]147$pbdebug = $debug;
148pb_log_init($debug, $LOG);
149pb_log(0,"Starting cb\n");
[1466]150
[1485]151# Get conf file in context
152pb_conf_init($appname);
153# The personal one if there is such
154pb_conf_add("$ENV{'HOME'}/.cbrc") if (-f "$ENV{'HOME'}/.cbrc");
155# The system one
156pb_conf_add(cb_env_conffile());
[1466]157
[1485]158# Get configuration parameters
159my %cb;
[1489]160my $cbp = ();
[1485]161my $cb = \%cb;
162($cb->{'basedir'},$cb->{'usemachines'},$cb->{'cms'},$cb->{'database'}) = pb_conf_get("cbbasedir","cbusemachines","cbcms","cbdatabase");
163pb_log(2,"%cb: ",Dumper($cb));
[1466]164
[1487]165die "No machine should be given when using machine structure" if (($cb->{'usemachines'}->{$appname} !~ /true/) && (defined $machine));
[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
[1485]205my $dest = "$ENV{'TMPDIR'}/vcs.$$";
[1487]206my $scheme = $cb->{'cms'}->{$appname};
207pb_vcs_export(pb_vcs_get_uri($scheme,$basedir),$basedir,$dest);
[1466]208
[1487]209# Now distribute to the right machines
[1489]210if (defined $machine) {
[1487]211 cb_distribute($machine);
[1485]212} else {
[1487]213 if ($cb->{'usemachines'}->{$appname} =~ /true/) {
214 # First dir level is the machine, then the content
215 opendir(DIR,$dest) || die "Unable to open $dest: $!";
216 foreach my $m (readdir(DIR)) {
217 next if ($m =~ /^\./);
218 next if (! -d $m);
219 # Machine name
220 cb_distribute($m);
221 }
222 closedir(DIR);
[1466]223 } else {
[1487]224 cb_distribute(undef);
[1466]225 }
226}
227
[1487]228# Cleanup
[1490]229if (not $debug) {
230 pb_rm_rf($dest);
231} else {
232 pb_log(0,"DEBUG: Please remove manually with rm -rf $dest\n")
233}
[1487]234$dbh->disconnect;
235
236# End of Main
237
238# Distribute files to target machines
239sub cb_distribute {
240
241my $machine = shift;
242
243pb_log(2,"Entering into cb_distribute with machine $machine\n");
244
245# Use potentially a remote account if defined
[1490]246my $remote = undef;
247my ($account) = pb_conf_get_if("cbaccount") if (defined $machine);
248$remote = $account->{$machine} if ((defined $account) && (defined $machine) && (defined $account->{$machine}));
249pb_log(1, "DEBUG: remote account1 = $remote\n") if (defined $remote);
250$remote = getpwuid($<) if (not defined $remote);
251pb_log(1, "DEBUG: remote account2 = $remote\n");
[1487]252
253# Now handle plugins if any
254if (defined $plugin) {
255 foreach my $p (split(/,/,$plugin)) {
256 pb_log(1,"Getting context for plugin $p\n");
257 $cbp = cb_plugin_get($p,$cbp);
258 # Adds mtime info to the plugin structure
[1494]259 foreach my $type ('files','dirs','dirsandfiles') {
[1487]260 foreach my $f (keys %{$cbp->{$p}->{$type}}) {
261 my $tdir = $dest;
262 if (defined $machine) {
263 $tdir = "$dest/$machine";
264 }
265 if (-r "$tdir/$f") {
266 my ($dev,$ino,$mode,$nlink,$uid,$gid,$rdev,$size,$atime,$mtime,$ctime,$blksize,$blocks) = stat("$tdir/$f") || die "Unable to stat $tdir: $!";
267 $cbp->{$p}->{$type}->{$f}->{'mtime'} = $mtime;
268 } else {
269 pb_log(0,"WARNING: Unable to read $tdir/$f from plugin $p\n");
270 }
271 }
272 }
273 }
[1466]274}
275
[1487]276# Handle this source
277if (defined $source) {
278 my $fullsource = "$source";
279 $fullsource = "$machine/$source" if (defined $machine);
280 pb_log(2,"fullsource is $fullsource\n");
281 my ($dev,$ino,$mode,$nlink,$uid,$gid,$rdev,$size,$atime,$mtime,$ctime,$blksize,$blocks) = stat("$dest/$fullsource") || die "Unable to stat $fullsource: $!";
282 my $type = 'files';
283 if (-d $fullsource) {
[1494]284 $type = 'dirsandfiles';
[1487]285 }
286 die "ERROR: Only able to handle files or dirs with option --source\n" if ((! -d $fullsource) && (! -f $fullsource));
[1466]287
[1487]288 $cbp->{"cb.source"}->{$type}->{$source}->{'uid'} = $uid;
289 $cbp->{"cb.source"}->{$type}->{$source}->{'gid'} = $gid;
290 $cbp->{"cb.source"}->{$type}->{$source}->{'mode'} = $mode;
291 $cbp->{"cb.source"}->{$type}->{$source}->{'mtime'} = $mtime;
292}
[1466]293
[1487]294if ((not defined $source) && (not defined $plugin)) {
295 # Here we need to take all content under $dest considering that machine
296 if (defined $machine) {
297 find(\&cb_add_to_cbp,("$dest/$machine"));
[1466]298 } else {
[1487]299 find(\&cb_add_to_cbp,($dest));
[1466]300 }
301}
[1489]302pb_log(2,"INFO: RAW cbp: ".Dumper(%$cbp)."\n");
[1466]303
[1487]304# Clean up cbp structure by comparing with data stored in the DB
305# Only keep the more recent modified content
306my $mac = $machine;
307$mac = "localhost" if (not defined $machine);
[1490]308# Allow for errors to occur at DBI level
309$dbh->{RaiseError} = 0;
310my $checkdb = 1;
311my $dbcmd = "SELECT id,date,file,machine FROM dates WHERE machine=\"$mac\"";
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();
321 # Check what in cbp is in the DB and deploy only if necessary
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;
343if (defined $machine) {
344 $tdir = "$dest/$machine";
345} else {
346 $tdir = "$dest";
347}
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]352$tar->setcwd($tdir);
353my $curdate = time();
[1489]354foreach my $k (keys %{$cbp}) {
[1494]355 foreach my $type ('files','dirs','dirsandfiles') {
356 # TODO: for dirs we may remove the files below ?
[1490]357 foreach my $o ((keys %{$cbp->{$k}->{$type}})) {
358 if (not defined $cbp->{$k}->{$type}->{$o}->{'deleted'}) {
359 $tar->add_files("./$o");
360 # Add an entry to the DB
361 if (defined $dbid->{$o}) {
362 # Modify an existing entry
363 $dbcmd = "UPDATE dates SET date=$curdate,file=$o WHERE id='?'";
364 if (not $debug) {
365 $sth = $dbh->prepare(qq{$dbcmd});
366 $sth = $dbh->execute($dbid->{$o});
367 } else {
368 pb_log(0,"Executing in DB: $dbcmd with curdate=$curdate,file=$o,id=$dbid->{$o}\n");
369 }
370 } else {
371 # Add an new entry
372 $dbcmd = "INSERT INTO dates VALUES (NULL,?,?,$mac)";
373 if (not $debug) {
374 $sth = $dbh->prepare(qq{$dbcmd});
375 $sth = $dbh->execute($curdate,$o);
376 } else {
377 pb_log(0,"Executing in DB: $dbcmd with curdate=$curdate,file=$o,machine=$mac\n");
378 }
379 }
380 if (not $debug) {
381 $sth->finish();
382 }
383 }
384 }
385 }
[1489]386}
[1490]387my $tarfile = "$ENV{'TMPDIR'}/cbcontent$$.tar";
[1489]388$tar->write("$tarfile");
[1466]389
[1490]390my $ssh2;
391my $chan;
392
393# deal with content first
[1485]394if (defined $machine) {
[1490]395 # Create remote connection and copy tar file there
396 $ssh2 = Net::SSH2->new();
397 $ssh2->connect($machine);
398 my $hdir = (getpwnam(getpwuid($<)))[7];
399 if ($ssh2->auth_publickey($remote,"$hdir/.ssh/id_dsa.pub","$hdir/.ssh/id_dsa")) {
400 $chan = $ssh2->channel();
401 if (not $debug) {
402 $chan->exec("if [ ! -d $ENV{'TMPDIR'} ]; then mkdir -p $ENV{'TMPDIR'} fi");
403 $ssh2->scp_put($tarfile,$ENV{'TMPDIR'});
404 #$chan->exec("cd / ; tar xf $tarfile");
405 } else {
406 pb_log(1,"INFO: creating $ENV{'TMPDIR'} if needed on $remote\@$machine and copying content\n");
407 }
408 } else {
409 pb_log(0,"ERROR: Unable to authenticate to $remote\@$machine\n");
410 return;
411 }
412}
413
414# Pointer to function depending whether we're local or remote
415my $func;
416if (defined $machine) {
417 $func = \&{ $chan->exec };
[1466]418} else {
[1490]419 $func = \&pb_system;
[1466]420}
421
[1490]422if (not $debug) {
423 &$func("cd / ; tar xf $tarfile");
[1466]424} else {
[1490]425 pb_log(1,"INFO: Extracting (on $mac) $tarfile under /\n");
[1466]426}
[1490]427
428foreach my $k (keys %{$cbp}) {
[1494]429 foreach my $type ('files','dirs','dirsandfiles') {
430 # TODO: do we act recursively for dirsandfiles at least for uid/gid ?
[1490]431 foreach my $o ((keys %{$cbp->{$k}->{$type}})) {
432 if (not defined $cbp->{$k}->{$type}->{$o}->{'deleted'}) {
433 if ($debug) {
434 pb_log(1,"INFO: Executing (on $mac) sudo chown $cbp->{$k}->{$type}->{$o}->{'uid'}:$cbp->{$k}->{$type}->{$o}->{'gid'} $o\n");
435 pb_log(1,"INFO: Executing (on $mac) sudo chmod $cbp->{$k}->{$type}->{$o}->{'mode'} $o\n");
436 } else {
437 &$func("sudo chown $cbp->{$k}->{$type}->{$o}->{'uid'}:$cbp->{$k}->{$type}->{$o}->{'gid'} $o");
438 &$func("sudo chmod $cbp->{$k}->{$type}->{$o}->{'mode'} $o");
439 }
440 }
441 }
442 }
443 if (defined $cbp->{$k}->{'reloadscript'}) {
444 if ($debug) {
[1494]445 pb_log(1,"INFO: Executing (on $mac) $cbp->{$k}->{'reloadscript'}\n");
[1490]446 } else {
[1494]447 &$func("$cbp->{$k}->{'reloadscript'}");
[1490]448 }
449 }
450}
451
452if (defined $machine) {
453 # Remote cleanup
454 $chan->exec("rm -rf $ENV{'TMPDIR'}");
455 $ssh2->disconnect();
456}
457
458# Cleanup
459if (not $debug) {
460 unlink("$tarfile");
461} else {
462 pb_log(0,"DEBUG: Please remove manually with rm -f $tarfile\n")
463}
[1487]464pb_log(2,"Exiting cb_distribute\n");
[1466]465}
[1487]466
467sub cb_add_to_cbp {
468
[1490]469pb_log(3,"Entering into cb_addto_cbp\n");
[1487]470my $type = 'files';
471if (-d $File::Find::name) {
472 $type = 'dirs';
473}
474
475my $destname = $File::Find::name;
476# Target name is without the $dest part
477$destname =~ s|^$dest||;
478my ($dev,$ino,$mode,$nlink,$uid,$gid,$rdev,$size,$atime,$mtime,$ctime,$blksize,$blocks) = stat("$File::Find::name") || die "Unable to stat $File::Find::name: $!";
479$cbp->{"cb.full"}->{$type}->{$destname}->{'uid'} = $uid;
480$cbp->{"cb.full"}->{$type}->{$destname}->{'gid'} = $gid;
481$cbp->{"cb.full"}->{$type}->{$destname}->{'mode'} = $mode;
482$cbp->{"cb.full"}->{$type}->{$destname}->{'mtime'} = $mtime;
483pb_log(2,"Adding $destname ($uid,$gid,$mode) to cbp\n");
484}
Note: See TracBrowser for help on using the repository browser.