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
Line 
1#!/usr/bin/perl -w
2#
3=head1 NAME
4
5cb - CasparBuster looks at the structure in your CMS environment and deploy it to the target systems as needed
6
7=head1 SYNOPSIS
8
9cb [options]
10
11 Options:
12 --debug |-d debug mode
13 --help |-h brief help message
14 --man full documentation
15 --force |-f force copy of files, even if they exist
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.
19
20=head1 OPTIONS
21
22=over 4
23
24=item B<--debug>
25
26Enter debug mode. This will print what would be done. No commands are executed,
27so this is safe to use when testing.
28
29=item B<--help>
30
31Print a brief help message and exits.
32
33=item B<--man>
34
35Prints the manual page and exits.
36
37=item B<--machine> I<machine name>
38
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
42When no machine is given, all machnes available are processed
43
44=item B<--source> I<path>
45
46Specify the path of the source file or directory to deploy with CasparBuster. Multiple paths can be specified separated by ','.
47
48=item B<--plugin> I<name>
49
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.
52
53=back
54
55=head1 DESCRIPTION
56
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.
58
59=head1 EXAMPLES
60
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
65
66 cb -m victoria2 -p dhcpd
67
68=head1 AUTHOR
69
70=over 4
71
72Bruno Cornec, http://brunocornec.wordpress.com
73
74=back
75
76=head1 LICENSE
77
78Copyright (C) 2012 Bruno Cornec <bruno@project-builder.org>
79Released under the GPLv2 or the Artistic license at your will.
80
81=cut
82use strict;
83use CasparBuster::Version;
84use CasparBuster::Env;
85use CasparBuster::Plugin;
86#use Cwd 'realpath';
87use Carp qw/confess cluck/;
88use File::Find;
89use Archive::Tar;
90use Getopt::Long;
91use Pod::Usage;
92use Data::Dumper;
93use Time::Local;
94use Net::SSH2;
95use ProjectBuilder::Base;
96use ProjectBuilder::Conf;
97use ProjectBuilder::VCS;
98use DBI;
99use DBD::SQLite;
100
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;
112my $findtarget = undef;
113
114my ($cbver,$cbrev) = cb_version_init();
115my $appname = "cb";
116$ENV{'PBPROJ'} = $appname;
117
118# Initialize the syntax string
119pb_syntax_init("$appname (aka CasparBuster) Version $cbver-$cbrev\n");
120
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,
132) || pb_syntax(-1,0);
133
134if (defined $help) {
135 pb_syntax(0,1);
136}
137if (defined $man) {
138 pb_syntax(0,2);
139}
140if (defined $quiet) {
141 $debug=-1;
142}
143if (defined $log) {
144 open(LOG,"> $log") || die "Unable to log to $log: $!";
145 $LOG = \*LOG;
146 $debug = 0 if ($debug == -1);
147}
148
149pb_log_init($debug, $LOG);
150pb_temp_init();
151pb_log(1,"Starting cb\n");
152
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());
159
160# Get configuration parameters
161my %cb;
162my $cbp = ();
163my $cb = \%cb;
164($cb->{'basedir'},$cb->{'cms'},$cb->{'database'}) = pb_conf_get("cbbasedir","cbcms","cbdatabase");
165pb_log(2,"%cb: ",Dumper($cb));
166
167if (defined $plugin) {
168 # Load plugins
169 cb_plugin_load();
170}
171
172my $basedir = $cb->{'basedir'}->{$appname};
173eval { $basedir =~ s/(\$ENV.+\})/$1/eeg };
174
175# Create basedir if it doesn't exist
176die "Unable to find base directory at $basedir" if (not -d $basedir);
177
178pb_log(1, "DEBUG MODE, not doing anything, just printing\nDEBUG: basedir = $basedir\n");
179
180# Create database if not existing and give a handler
181my $db = "$basedir/$cb->{'database'}->{$appname}";
182
183my $precmd = "";
184if (! -f $db) {
185 $precmd = "CREATE TABLE dates (id INTEGER PRIMARY KEY AUTOINCREMENT, date INTEGER, file VARCHAR[65535], machine VARCHAR[65535])";
186}
187
188my $dbh = DBI->connect("dbi:SQLite:dbname=$db","","",
189 { RaiseError => 1, AutoCommit => 1 })
190 || die "Unable to connect to $db";
191my $sth;
192
193if ($precmd ne "") {
194 $sth = $dbh->prepare(qq{$precmd}) || die "Unable to create table into $db";
195 if ($debug) {
196 pb_log(1,"DEBUG: Creating DB $db\n");
197 pb_log(1,"DEBUG: with command $precmd\n");
198 } else {
199 $sth->execute();
200 }
201 $sth->finish();
202}
203
204# Define destination dir and populate with a VCS export
205my $dest = "$ENV{'PBTMP'}/vcs.$$";
206my $scheme = $cb->{'cms'}->{$appname};
207
208# Avoids too many permission changes
209umask(0022);
210pb_vcs_export(pb_vcs_get_uri($scheme,$basedir),$basedir,$dest);
211
212# Now distribute to the right machines
213if (defined $machine) {
214 cb_distribute($machine);
215} else {
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);
225 }
226}
227
228# Cleanup
229$dbh->disconnect;
230pb_exit();
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");
240confess "No machine fiven to cb_distribute" if (not defined $machine);
241
242# Use potentially a remote account if defined
243my $remote = undef;
244my ($account) = pb_conf_get_if("cbaccount");
245$remote = $account->{$machine} if ((defined $account) && (defined $account->{$machine}));
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");
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
256 foreach my $type ('files','dirs','dirsandfiles') {
257 foreach my $f (keys %{$cbp->{$p}->{$type}}) {
258 my $tdir = "$dest/$machine";
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 }
268}
269
270# Handle this source
271if (defined $source) {
272 my $fullsource = "$source";
273 $fullsource = "$machine/$source";
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) {
278 $type = 'dirsandfiles';
279 }
280 die "ERROR: Only able to handle files or dirs with option --source\n" if ((! -d $fullsource) && (! -f $fullsource));
281
282 # We should get uid/gid fro elsewhere as they're probably wrong locally
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}
288
289($cb->{'commondir'},$cb->{'websrv'},$cb->{'webdir'}) = pb_conf_get_if("cbcommondir","cbwebsrv","cbwebdir");
290
291if ((not defined $source) && (not defined $plugin)) {
292 # Here we need to take all content under $dest considering that machine
293 $findtarget = "$dest/$machine";
294 find(\&cb_add_to_cbp,($findtarget));
295}
296pb_log(1,"INFO: RAW cbp: ".Dumper(%$cbp)."\n");
297
298# Clean up cbp structure by comparing with data stored in the DB
299# Only keep the more recent modified content
300# Allow for errors to occur at DBI level
301$dbh->{RaiseError} = 0;
302my $checkdb = 1;
303my $dbcmd = "SELECT id,date,file,machine FROM dates WHERE machine=\"$machine\"";
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();
313 # Check what in cbp is in the DB and deploy only if necessary or forced
314 foreach my $k (keys %{$cbp}) {
315 foreach my $type ('files','dirs','dirsandfiles') {
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 }
325 }
326 }
327 }
328 $sth->finish();
329}
330pb_log(2,"INFO: cleaned cbp: ".Dumper($cbp)."\n");
331
332# Now create a tar containing all the relevant content
333# We need to loop separately to allow for DB to not exist in the previous loop !
334my $tdir = undef;
335$tdir = "$dest/$machine";
336chdir("$tdir") || die "ERROR: Unable to chdir to $tdir\n";
337pb_log(2,"Working now under $tdir\n");
338
339my $tar = Archive::Tar->new;
340$tar->setcwd($tdir);
341my $curdate = time();
342foreach my $k (keys %{$cbp}) {
343 foreach my $type ('files','dirs','dirsandfiles') {
344 # TODO: for dirs we may remove the files below ?
345 foreach my $o ((keys %{$cbp->{$k}->{$type}})) {
346 if ((defined $force) || (not defined $cbp->{$k}->{$type}->{$o}->{'deleted'})) {
347 $tar->add_files("$o");
348 # Add an entry to the DB
349 if (defined $dbid->{$o}) {
350 # Modify an existing entry
351 $dbcmd = "UPDATE dates SET date=\"$curdate\",file=\"$o\" WHERE id=\"$dbid->{$o}\"";
352 if (not $debug) {
353 $sth = $dbh->prepare(qq{$dbcmd});
354 $sth->execute();
355 }
356 pb_log(0,"Executing in DB: $dbcmd with curdate=$curdate,file=$o,id=$dbid->{$o}\n");
357 } else {
358 # Add an new entry
359 $dbcmd = "INSERT INTO dates VALUES (NULL,?,?,\"$machine\")";
360 if (not $debug) {
361 $sth = $dbh->prepare(qq{$dbcmd});
362 $sth->execute($curdate,$o);
363 }
364 pb_log(0,"Executing in DB: $dbcmd with curdate=$curdate,file=$o,machine=$machine\n");
365 }
366 if (not $debug) {
367 $sth->finish();
368 }
369 }
370 }
371 }
372}
373my $tarfile = "$ENV{'PBTMP'}/cbcontent$$.tar";
374$tar->write("$tarfile");
375
376my $ssh2;
377my $chan;
378
379# deal with content first
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};
388}
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;
395
396 $ssh2->scp_put($tarfile,$tarfile);
397 pb_log(0,"INFO: Created $ENV{'PBTMP'} on $remote\@$mach and copying content\n");
398} else {
399 pb_log(0,"ERROR: Unable to authenticate to $remote\@$mach\n");
400 return;
401}
402
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();
411if (not $debug) {
412 print $chan "sudo tar -C $path -x -f $tarfile $tbextract\n";
413 pb_log(0,"WARNING: $_\n") while (<$chan>);
414} else {
415 print $chan "tar -C $path -t -f $tarfile $tbextract\n";
416 pb_log(2,"INFO: tar content: $_") while (<$chan>);
417}
418
419pb_log(0,"INFO: Extracting $tbextract (on $machine) $tarfile under $path\n");
420
421foreach my $k (keys %{$cbp}) {
422 foreach my $type ('files','dirs','dirsandfiles') {
423 # TODO: do we act recursively for dirsandfiles at least for uid/gid ?
424 foreach my $o ((keys %{$cbp->{$k}->{$type}})) {
425 # Note that $path/$o is remote only
426 if ((defined $force) || (not defined $cbp->{$k}->{$type}->{$o}->{'deleted'})) {
427 if ($debug) {
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");
430 } else {
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";
434 }
435 pb_log(0,"INFO: Delivering $path/$o on $machine\n");
436 }
437 }
438 }
439 if (defined $cbp->{$k}->{'reloadscript'}) {
440 if (not $debug) {
441 print $chan "$cbp->{$k}->{'reloadscript'}\n";
442 }
443 pb_log(0,"INFO: Executing (on $machine) $cbp->{$k}->{'reloadscript'}\n");
444 }
445}
446
447# Remote cleanup
448if (not $debug) {
449 print $chan "rm -rf $ENV{'PBTMP'}\n";
450} else {
451 pb_log(1,"Remove remote directory $ENV{'PBTMP'} on $machine\n");
452}
453$chan->close();
454$ssh2->disconnect();
455
456chdir("/");
457pb_log(2,"Exiting cb_distribute\n");
458}
459
460sub cb_add_to_cbp {
461
462pb_log(3,"Entering into cb_add_to_cbp\n");
463my $type = 'files';
464if (-d $File::Find::name) {
465 $type = 'dirs';
466}
467
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");
482}
Note: See TracBrowser for help on using the repository browser.