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

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