File Coverage

blib/lib/Apache/Voodoo/Install/Updater.pm
Criterion Covered Total %
statement 25 27 92.5
branch n/a
condition n/a
subroutine 9 9 100.0
pod n/a
total 34 36 94.4


line stmt bran cond sub pod time code
1             ###############################################################################
2             #
3             # Apache::Voodoo::Install::Updater - Update xml processor
4             #
5             # This package provides the internal methods use by voodoo-control that do
6             # pre/post/upgrade commands as specified by the various .xml files in an
7             # application. It's not intended to be use directly by end users.
8             #
9             ###############################################################################
10             package Apache::Voodoo::Install::Updater;
11              
12             $VERSION = "3.0200";
13              
14 1     1   1197 use strict;
  1         2  
  1         38  
15 1     1   5 use warnings;
  1         2  
  1         29  
16              
17 1     1   5 use base("Apache::Voodoo::Install");
  1         2  
  1         76  
18              
19 1     1   7 use Apache::Voodoo::Constants;
  1         2  
  1         28  
20              
21 1     1   5501 use CPAN;
  1         363619  
  1         581  
22 1     1   30 use DBI;
  1         7  
  1         60  
23 1     1   15 use Digest::MD5;
  1         5  
  1         56  
24 1     1   12 use Sys::Hostname;
  1         6  
  1         70  
25 1     1   639 use XML::Checker::Parser;
  0            
  0            
26             use File::Find;
27             use Config::General qw(ParseConfig);
28              
29             # make CPAN download dependancies
30             $CPAN::Config->{'prerequisites_policy'} = 'follow';
31              
32             ################################################################################
33             # Creates a new updater object with given configuration options. It assumes
34             # that the files for the application have already been installed or exist in
35             # appropriate location. A good database security setup would not allow the
36             # user the application connects as to have alter, create or drop privileges; thus
37             # the need for the database root password. If pretend is set to a true value,
38             # the operations are stepped through, but nothing actually happens.
39             #
40             # usage:
41             # Apache::Voodoo::Install::Updater->new(
42             # dbroot => $database_root_password,
43             # app_name => $application_name,
44             # verbose => $output_verbosity_level,
45             # pretend => $boolean
46             # );
47             ################################################################################
48             sub new {
49             my $class = shift;
50             my %params = @_;
51              
52             my $self = {%params};
53              
54             my $ac = Apache::Voodoo::Constants->new();
55             $self->{'_md5_'} = Digest::MD5->new;
56              
57             $self->{'install_path'} = $ac->install_path()."/".$self->{'app_name'};
58              
59             $self->{'conf_file'} = $self->{'install_path'}."/".$ac->conf_file();
60             $self->{'conf_path'} = $self->{'install_path'}."/".$ac->conf_path();
61             $self->{'updates_path'} = $self->{'install_path'}."/".$ac->updates_path();
62             $self->{'apache_uid'} = $ac->apache_uid();
63             $self->{'apache_gid'} = $ac->apache_gid();
64              
65             unless (-e $self->{'conf_file'}) {
66             die "Can't open configuration file: $self->{'conf_file'}\n";
67             }
68              
69             bless $self, $class;
70              
71             return $self;
72             }
73              
74             # Causes the update chain to execute: pre-setup.xml, unapplied updates, post-setup.xml
75             sub do_update { $_[0]->_do_all(0); }
76              
77             # Causes the new install chain to execute: pre-setup.xml, setup.xml,
78             # post-setup.xml, mark all updates applied. If this is executed on an
79             # existing system, Bad Things(tm) can happen depending on what commands
80             # are present in setup.xml
81             sub do_new_install { $_[0]->_do_all(1); }
82              
83             # Wizard mode function. This performs a replace into on the _updates table
84             # of a system to have entries and correct checksums for each update file
85             # without actually executing them. If something went wrong with an install or
86             # upgrade and manual tinkering was required to get things back in order, this
87             # method can be used to ensure that the _updates table appears current.
88             sub mark_updates_applied {
89             my $self = shift;
90              
91             my %conf = ParseConfig($self->{'conf_file'});
92              
93             $self->mesg("- Connection to database");
94             $self->{'dbh'} = DBI->connect($conf{'database'}->{'connect'},'root',$self->{'dbroot'}) || die DBI->errstr;
95              
96             $self->mesg("- Looking for update command xml files");
97              
98             $self->_record_updates($self->_find_updates());
99              
100             $self->mesg("- All updates marked as applied");
101             }
102              
103             sub _do_all {
104             my $self = shift;
105             my $new = shift;
106              
107             my %conf = ParseConfig($self->{'conf_file'});
108              
109             if ($new) {
110             $self->mesg("- Creating database");
111              
112             # FIXME create a factory structure to support multiple database types.
113             my $c = $conf{'database'}->{'connect'};
114             my ($dbname) = ($c =~/database=([^;]+)/);
115             $c =~ s/database=[^;]+/database=test/;
116              
117             $self->{'dbh'} = DBI->connect($c,'root',$self->{'dbroot'}) || die DBI->errstr;
118             $self->{'dbh'}->do("CREATE DATABASE $dbname"); # allowed to silently fail, db may already exist
119             $self->{'dbh'}->disconnect;
120              
121             $self->{'dbh'} = DBI->connect($conf{'database'}->{'connect'},'root',$self->{'dbroot'}) || die DBI->errstr;
122              
123             $self->mesg("- Looking for setup command xml files");
124             }
125             else {
126             $self->mesg("- Connection to database");
127             $self->{'dbh'} = DBI->connect($conf{'database'}->{'connect'},'root',$self->{'dbroot'}) || die DBI->errstr;
128              
129             $self->mesg("- Looking for update command xml files");
130             }
131              
132             # even if this is a new installation, we still need a list of the update files that came with
133             # this distribution so that we'll know what updates to *not* perform in the event of an upgrade
134             my @updates = $self->_find_updates();
135              
136             my @files;
137             push(@files,$self->_find('pre-setup'));
138              
139             if ($new) {
140             push(@files,$self->_find('setup'));
141             }
142             else {
143             push(@files,@updates);
144             }
145              
146             push(@files,$self->_find('post-setup'));
147              
148             # remove any "gaps". There might not have been a pre/post/setup file.
149             @files = grep { defined($_) } @files;
150              
151             my @commands = $self->_parse_commands(@files);
152              
153             $self->_execute_commands(@commands);
154              
155             # as noted above. even for new installs we need to keep track of what updates
156             # were part of this distro so we don't do them on the next update.
157             $self->_record_updates(@updates);
158             }
159              
160             sub _find {
161             my $self = shift;
162             my $file = shift;
163              
164             my $path = $self->{'conf_path'};
165             if (-e "$path/$file.xml") {
166             $self->debug(" $file.xml");
167             return "$path/$file.xml";
168             }
169              
170             return undef;
171             }
172              
173             sub _find_updates {
174             my $self = shift;
175              
176             return () unless (-e $self->{'updates_path'});
177              
178             my @updates;
179             find({
180             wanted => sub {
181             my $file = $_;
182             if ($file =~ /\d+\.\d+\.\d+(-[a-z\d]+)?\.xml$/) {
183             push(@updates,$file);
184             }
185             },
186             no_chdir => 1,
187             follow => 1
188             },
189             $self->{'updates_path'}
190             );
191              
192             # Swartzian transform
193             @updates = map {
194             $_->[0]
195             }
196             sort {
197             $a->[1] <=> $b->[1] ||
198             $a->[2] <=> $b->[2] ||
199             $a->[3] <=> $b->[3] ||
200             defined($b->[4]) <=> defined($a->[4]) ||
201             $a->[4] cmp $b->[4]
202             }
203             map {
204             my $f = $_;
205             s/.*\///;
206             s/\.xml$//;
207             [ $f , split(/[\.-]/,$_) ]
208             }
209             @updates;
210              
211             $self->_touch_updates_table();
212              
213             return grep { ! $self->_is_applied($_) } @updates;
214             }
215              
216             sub _touch_updates_table {
217             my $dbh = $_[0]->{'dbh'};
218              
219             my $res = $dbh->selectall_arrayref("SHOW TABLES LIKE '_updates'");
220             unless (defined($res->[0]) && $res->[0]->[0] eq "_updates") {
221             # not there. create it.
222             $dbh->do("
223             CREATE TABLE _updates (
224             file VARCHAR(255) NOT NULL PRIMARY KEY,
225             checksum VARCHAR(32) NOT NULL
226             )") || die DBI->errstr;
227             }
228             }
229              
230             sub _record_updates {
231             my $self = shift;
232             my @files = @_;
233              
234             $self->_touch_updates_table();
235              
236             my $dbh = $self->{'dbh'};
237              
238             foreach my $file (@files) {
239             my $sum = $self->_md5_checksum($file);
240             $file =~ s/.*\///;
241             $file =~ s/\.xml//;
242             $dbh->do("REPLACE INTO _updates(file,checksum) VALUES(?,?)",undef,$file,$sum) || die DBI->errstr;
243             }
244             }
245              
246             sub _is_applied {
247             my $self = shift;
248             my $file = shift;
249              
250             my $dbh = $self->{'dbh'};
251              
252             my $f = $file;
253             $f =~ s/.*\///;
254             $f =~ s/\.xml//;
255              
256             my $res = $dbh->selectall_arrayref("
257             SELECT
258             checksum
259             FROM
260             _updates
261             WHERE
262             file = ?",undef,
263             $f) || die DBI->errstr;
264              
265             if (defined($res->[0]->[0])) {
266             if ($res->[0]->[0] ne $self->_md5_checksum($file)) {
267             # YIKES!!! this update file doesn't match the one
268             # we think we've already ran.
269             print "MD5 checksum of $f doesn't match the one store in the DB. aborting\n";
270             exit;
271             }
272             else {
273             return 1;
274             }
275             }
276             else {
277             return 0;
278             }
279             }
280              
281             sub _md5_checksum {
282             my $self = shift;
283             my $file = shift;
284              
285             my $md5 = $self->{'_md5_'};
286             $md5->reset;
287              
288             open(F,$file) || die "Can't md5 file $file: $!";
289             $md5->add();
290             close(F);
291              
292             return $md5->hexdigest;
293             }
294              
295             sub _parse_commands {
296             my $self = shift;
297              
298             my @commands;
299             foreach my $file (@_) {
300             my $data = $self->_parse_xml($file);
301              
302             if (!defined($data)) {
303             print "\n* Parse of $file failed. Aborting *\n";
304             exit;
305             }
306             print " parsed $file\n";
307             push(@commands,[$file,$data]);
308             }
309             return @commands;
310             }
311              
312             sub _parse_xml {
313             my $self = shift;
314             my $xmlfile = shift;
315              
316             my $parser = new XML::Checker::Parser(
317             'Style' => 'Tree',
318             'SkipInsignifWS' => 1
319             );
320              
321             my $dtdpath = $INC{'Apache/Voodoo/Install/Updater.pm'};
322             $dtdpath =~ s/Install\/Updater\.pm$//;
323              
324             $parser->set_sgml_search_path($dtdpath);
325              
326             my $data;
327             eval {
328             # parser checker only dies on catastrophic errors. Adding this handler
329             # makes it die on ALL errors.
330             local $XML::Checker::FAIL = sub {
331             my $errcode = shift;
332              
333             print "\n ** Parse of $xmlfile failed **\n";
334             die XML::Checker::error_string ($errcode, @_) if $errcode < 200;
335             XML::Checker::print_error ($errcode, @_);
336             };
337              
338             $data = $parser->parsefile($xmlfile);
339             };
340             if ($@) {
341             print $@;
342             return undef;
343             }
344             return $data;
345              
346             }
347              
348             sub _execute_commands {
349             my $self = shift;
350             my @set = @_;
351              
352             my $pretend = $self->{'pretend'};
353             my $install_path = $self->{'install_path'};
354              
355             chdir($install_path);
356              
357             # find out what our hostname is
358             my $hostname = Sys::Hostname::hostname();
359              
360             $self->info("- Running setup/update commands");
361             foreach (@set) {
362             my $file = $_->[0];
363             my @commands = @{$_->[1]->[1]};
364              
365             $self->debug(" $file");
366             for (my $i=1; $i < $#commands; $i+=2) {
367             my $type = $commands[$i];
368             my $data = $commands[$i+1]->[2];
369              
370             if (defined($commands[$i+1]->[0]->{'onhosts'})) {
371             next unless grep { /^$hostname$/ } split(/\s*,\s*/,$commands[$i+1]->[0]->{'onhosts'});
372             }
373              
374             # Reset the current working directory back to the install path
375             chdir($install_path);
376              
377             $data =~ s/^\s*//;
378             $data =~ s/\s*$//;
379              
380             if ($type eq "shell") {
381             $self->debug(" SHELL: ", $data);
382             unless ($self->{pretend}) {
383             if (system($data)) {
384             $self->{ignore} or die "Shell command failed: $!";
385             }
386             }
387             }
388             elsif ($type eq "sql") {
389             $self->_execute_sql($data);
390             }
391             elsif ($type eq "mkdir") {
392             $self->debug(" MKDIR: ", $data);
393             $self->make_writeable_dirs("$install_path/$data");
394             }
395             elsif ($type eq "mkfile") {
396             $self->debug(" TOUCH/CHMOD: ", $data);
397             $self->make_writeable_files("$install_path/$data");
398             }
399             elsif ($type eq "install") {
400             $self->debug(" CPAN Install: ", $data);
401             unless ($pretend) {
402             CPAN::Shell->install($data);
403             }
404             }
405             else {
406             print "\n* Unsupported command type ($type). Aborting *\n";
407             exit;
408             }
409             }
410             }
411             }
412              
413             sub _execute_sql {
414             my $self = shift;
415             my $data = shift;
416              
417             $self->debug(" SQL: ", $data);
418             return if $self->{'pretend'};
419              
420             my $path = $self->{'install_path'};
421             my $dbh = $self->{'dbh'};
422              
423             if ($data =~ /^source\s/i) {
424             $data =~ s/^source\s*//i;
425              
426             my ($query,$in_quote,$close_quote);
427             open(SQL,"$path/$data") || die "Can't open $path/$data: $!";
428             while (!eof(SQL)) {
429             my $c = getc SQL;
430             if (!$in_quote && $c eq ';') {
431             $query =~ s/^\s*//;
432             $query =~ s/\s$//;
433             next if ($query =~ /^[\s;]*$/); # an empty query turns a do into a don't
434             next if ($query =~ /^(UN)?LOCK /i); # do yacks on these too
435              
436             unless ($dbh->do($query)) {
437             $self->{ignore} or die "sql source failed $query: " . DBI->errstr;
438             }
439              
440             $query = '';
441             $c = getc SQL;
442             }
443              
444             if ($c eq '\\') {
445             $query .= $c;
446             $c = getc SQL; # automatically add the next character
447             }
448             elsif ($c eq "'") {
449             if ($in_quote && $close_quote eq "'") {
450             $in_quote = 0;
451             $close_quote = '';
452             }
453             elsif (!$in_quote) {
454             $in_quote = 1;
455             $close_quote = "'";
456             }
457             }
458             elsif ($c eq '"') {
459             if ($in_quote && $close_quote eq '"') {
460             $in_quote = 0;
461             $close_quote = '';
462             }
463             elsif (!$in_quote) {
464             $in_quote = 1;
465             $close_quote = '"';
466             }
467             }
468              
469             $query .= $c;
470             }
471             close(SQL);
472             }
473             else {
474             unless ($dbh->do($data)) {
475             $self->{ignore} or die "sql failed: DBI->errstr\n\n$data";
476             }
477             }
478             }
479              
480             1;
481              
482             ################################################################################
483             # Copyright (c) 2005-2010 Steven Edwards (maverick@smurfbane.org).
484             # All rights reserved.
485             #
486             # You may use and distribute Apache::Voodoo under the terms described in the
487             # LICENSE file include in this package. The summary is it's a legalese version
488             # of the Artistic License :)
489             #
490             ################################################################################