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

Last change on this file since 1490 was 1490, checked in by Bruno Cornec, 12 years ago
  • Fixes in cb to make it work in debug mode at least with package dhcpd.
  • Property svn:executable set to *
File size: 13.6 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
42
43=item B<--source> I<path>
44
45Specify the path of the source file or directory to deploy with CasparBuster. Multiple paths can be specified separated by ','.
46
47=item B<--plugin> I<name>
48
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.
51
52=back
53
54=head1 DESCRIPTION
55
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.
57
58=head1 EXAMPLES
59
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
64
65 cb -m victoria2 -p dhcpd
66
67=head1 AUTHOR
68
69=over 4
70
71Bruno Cornec, http://brunocornec.wordpress.com
72
73=back
74
75=head1 LICENSE
76
77Copyright (C) 2012 Bruno Cornec <bruno@project-builder.org>
78Released under the GPLv2 or the Artistic license at your will.
79
80=cut
81use strict;
82use CasparBuster::Version;
83use CasparBuster::Env;
84use CasparBuster::Plugin;
85#use Cwd 'realpath';
86use File::Find;
87use Archive::Tar;
88use Getopt::Long;
89use Pod::Usage;
90use Data::Dumper;
91use Time::Local;
92use Net::SSH2;
93use ProjectBuilder::Base;
94use ProjectBuilder::Conf;
95use ProjectBuilder::VCS;
96use DBI;
97use DBD::SQLite;
98
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;
110
111my ($cbver,$cbrev) = cb_version_init();
112my $appname = "cb";
113$ENV{'PBPROJ'} = $appname;
114pb_temp_init();
115
116# Initialize the syntax string
117pb_syntax_init("$appname (aka CasparBuster) Version $cbver-$cbrev\n");
118
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,
130) || pb_syntax(-1,0);
131
132if (defined $help) {
133 pb_syntax(0,1);
134}
135if (defined $man) {
136 pb_syntax(0,2);
137}
138if (defined $quiet) {
139 $debug=-1;
140}
141if (defined $log) {
142 open(LOG,"> $log") || die "Unable to log to $log: $!";
143 $LOG = \*LOG;
144 $debug = 0 if ($debug == -1);
145}
146
147$pbdebug = $debug;
148pb_log_init($debug, $LOG);
149pb_log(0,"Starting cb\n");
150
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());
157
158# Get configuration parameters
159my %cb;
160my $cbp = ();
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));
164
165die "No machine should be given when using machine structure" if (($cb->{'usemachines'}->{$appname} !~ /true/) && (defined $machine));
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{'TMPDIR'}/vcs.$$";
206my $scheme = $cb->{'cms'}->{$appname};
207pb_vcs_export(pb_vcs_get_uri($scheme,$basedir),$basedir,$dest);
208
209# Now distribute to the right machines
210if (defined $machine) {
211 cb_distribute($machine);
212} else {
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);
223 } else {
224 cb_distribute(undef);
225 }
226}
227
228# Cleanup
229if (not $debug) {
230 pb_rm_rf($dest);
231} else {
232 pb_log(0,"DEBUG: Please remove manually with rm -rf $dest\n")
233}
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
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");
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
259 foreach my $type ('files','dirs') {
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 }
274}
275
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) {
284 $type = 'dirs';
285 }
286 die "ERROR: Only able to handle files or dirs with option --source\n" if ((! -d $fullsource) && (! -f $fullsource));
287
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}
293
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"));
298 } else {
299 find(\&cb_add_to_cbp,($dest));
300 }
301}
302pb_log(2,"INFO: RAW cbp: ".Dumper(%$cbp)."\n");
303
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);
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}) {
323 foreach my $type ('files','dirs') {
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 }
333 }
334 }
335 }
336 $sth->finish();
337}
338pb_log(2,"INFO: cleaned cbp: ".Dumper($cbp)."\n");
339
340# Now create a tar containing all the relevant content
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
351my $tar = Archive::Tar->new;
352$tar->setcwd($tdir);
353my $curdate = time();
354foreach my $k (keys %{$cbp}) {
355 foreach my $type ('files','dirs') {
356 foreach my $o ((keys %{$cbp->{$k}->{$type}})) {
357 if (not defined $cbp->{$k}->{$type}->{$o}->{'deleted'}) {
358 $tar->add_files("./$o");
359 # Add an entry to the DB
360 if (defined $dbid->{$o}) {
361 # Modify an existing entry
362 $dbcmd = "UPDATE dates SET date=$curdate,file=$o WHERE id='?'";
363 if (not $debug) {
364 $sth = $dbh->prepare(qq{$dbcmd});
365 $sth = $dbh->execute($dbid->{$o});
366 } else {
367 pb_log(0,"Executing in DB: $dbcmd with curdate=$curdate,file=$o,id=$dbid->{$o}\n");
368 }
369 } else {
370 # Add an new entry
371 $dbcmd = "INSERT INTO dates VALUES (NULL,?,?,$mac)";
372 if (not $debug) {
373 $sth = $dbh->prepare(qq{$dbcmd});
374 $sth = $dbh->execute($curdate,$o);
375 } else {
376 pb_log(0,"Executing in DB: $dbcmd with curdate=$curdate,file=$o,machine=$mac\n");
377 }
378 }
379 if (not $debug) {
380 $sth->finish();
381 }
382 }
383 }
384 }
385}
386my $tarfile = "$ENV{'TMPDIR'}/cbcontent$$.tar";
387$tar->write("$tarfile");
388
389my $ssh2;
390my $chan;
391
392# deal with content first
393if (defined $machine) {
394 # Create remote connection and copy tar file there
395 $ssh2 = Net::SSH2->new();
396 $ssh2->connect($machine);
397 my $hdir = (getpwnam(getpwuid($<)))[7];
398 if ($ssh2->auth_publickey($remote,"$hdir/.ssh/id_dsa.pub","$hdir/.ssh/id_dsa")) {
399 $chan = $ssh2->channel();
400 if (not $debug) {
401 $chan->exec("if [ ! -d $ENV{'TMPDIR'} ]; then mkdir -p $ENV{'TMPDIR'} fi");
402 $ssh2->scp_put($tarfile,$ENV{'TMPDIR'});
403 #$chan->exec("cd / ; tar xf $tarfile");
404 } else {
405 pb_log(1,"INFO: creating $ENV{'TMPDIR'} if needed on $remote\@$machine and copying content\n");
406 }
407 } else {
408 pb_log(0,"ERROR: Unable to authenticate to $remote\@$machine\n");
409 return;
410 }
411}
412
413# Pointer to function depending whether we're local or remote
414my $func;
415if (defined $machine) {
416 $func = \&{ $chan->exec };
417} else {
418 $func = \&pb_system;
419}
420
421if (not $debug) {
422 &$func("cd / ; tar xf $tarfile");
423} else {
424 pb_log(1,"INFO: Extracting (on $mac) $tarfile under /\n");
425}
426
427foreach my $k (keys %{$cbp}) {
428 foreach my $type ('files','dirs') {
429 foreach my $o ((keys %{$cbp->{$k}->{$type}})) {
430 if (not defined $cbp->{$k}->{$type}->{$o}->{'deleted'}) {
431 if ($debug) {
432 pb_log(1,"INFO: Executing (on $mac) sudo chown $cbp->{$k}->{$type}->{$o}->{'uid'}:$cbp->{$k}->{$type}->{$o}->{'gid'} $o\n");
433 pb_log(1,"INFO: Executing (on $mac) sudo chmod $cbp->{$k}->{$type}->{$o}->{'mode'} $o\n");
434 } else {
435 &$func("sudo chown $cbp->{$k}->{$type}->{$o}->{'uid'}:$cbp->{$k}->{$type}->{$o}->{'gid'} $o");
436 &$func("sudo chmod $cbp->{$k}->{$type}->{$o}->{'mode'} $o");
437 }
438 }
439 }
440 }
441 if (defined $cbp->{$k}->{'reloadscript'}) {
442 if ($debug) {
443 pb_log(1,"INFO: Executing (on $mac) sudo $cbp->{$k}->{'reloadscript'}\n");
444 } else {
445 &$func("sudo $cbp->{$k}->{'reloadscript'}");
446 }
447 }
448}
449
450if (defined $machine) {
451 # Remote cleanup
452 $chan->exec("rm -rf $ENV{'TMPDIR'}");
453 $ssh2->disconnect();
454}
455
456# Cleanup
457if (not $debug) {
458 unlink("$tarfile");
459} else {
460 pb_log(0,"DEBUG: Please remove manually with rm -f $tarfile\n")
461}
462pb_log(2,"Exiting cb_distribute\n");
463}
464
465sub cb_add_to_cbp {
466
467pb_log(3,"Entering into cb_addto_cbp\n");
468my $type = 'files';
469if (-d $File::Find::name) {
470 $type = 'dirs';
471}
472
473my $destname = $File::Find::name;
474# Target name is without the $dest part
475$destname =~ s|^$dest||;
476my ($dev,$ino,$mode,$nlink,$uid,$gid,$rdev,$size,$atime,$mtime,$ctime,$blksize,$blocks) = stat("$File::Find::name") || die "Unable to stat $File::Find::name: $!";
477$cbp->{"cb.full"}->{$type}->{$destname}->{'uid'} = $uid;
478$cbp->{"cb.full"}->{$type}->{$destname}->{'gid'} = $gid;
479$cbp->{"cb.full"}->{$type}->{$destname}->{'mode'} = $mode;
480$cbp->{"cb.full"}->{$type}->{$destname}->{'mtime'} = $mtime;
481pb_log(2,"Adding $destname ($uid,$gid,$mode) to cbp\n");
482}
Note: See TracBrowser for help on using the repository browser.