1 =head1 backup-config-lib.pl
3 Functions for creating configuration file backups. Some example code :
5 foreign_require('backup-config', 'backup-config-lib.pl');
6 @backups = backup_config::list_backups();
7 ($apache_backup) = grep { $_->{'mods'} eq 'apache' } @backups;
8 $apache_backup->{'dest'} = '/tmp/apache.tar.gz';
9 &backup_config::save_backup($apache_backup);
13 BEGIN { push(@INC, ".."); };
17 our (%text, $module_config_directory, %config);
19 &foreign_require("cron", "cron-lib.pl");
21 our $cron_cmd = "$module_config_directory/backup.pl";
22 our $backups_dir = "$module_config_directory/backups";
23 our $manifests_dir = "/tmp/backup-config-manifests";
25 =head2 list_backup_modules
27 Returns details of all modules that allow backups, each of which is a hash
28 ref in the same format as returned by get_module_info.
31 sub list_backup_modules
34 foreach $m (&get_all_module_infos()) {
35 my $mdir = &module_root_directory($m->{'dir'});
36 if (&check_os_support($m) &&
37 -r "$mdir/backup_config.pl") {
41 return sort { $a->{'desc'} cmp $b->{'desc'} } @rv;
46 Returns a list of all configured backups, each of which is a hash ref with
47 at least the following keys :
49 =item mods - Space-separate list of modules to include.
51 =item dest - Destination file, FTP or SSH server.
53 =item configfile - Set to 1 if /etc/webmin/modulename files are included.
55 =item nofiles - Set to 1 if server config files (like httpd.conf) are NOT included.
57 =item others - A tab-separated list of other files to include.
59 =item email -Email address to notify.
61 =item emode - Set to 0 to send email only on failure, 1 to always send.
63 =item sched - Set to 1 if regular scheduled backups are enabled.
65 =item mins,hours,days,months,weekdays - Cron-style specification of backup time.
71 opendir(DIR, $backups_dir) || return ();
72 foreach $f (sort { $a cmp $b } readdir(DIR)) {
73 next if ($f !~ /^(\S+)\.backup$/);
74 push(@rv, &get_backup($1));
82 Given a unique backup ID, returns a hash ref containing its details, in the
83 same format as list_backups.
89 &read_file("$backups_dir/$_[0].backup", \%backup) || return undef;
90 $backup{'id'} = $_[0];
94 =head2 save_backup(&backup)
96 Given a hash ref containing backup details, saves them to disk. Must be in
97 the same format as returned by list_backups, except for the ID which will be
98 randomly assigned if missing.
103 $_[0]->{'id'} ||= time().$$;
104 mkdir($backups_dir, 0700);
105 &lock_file("$backups_dir/$_[0]->{'id'}.backup");
106 &write_file("$backups_dir/$_[0]->{'id'}.backup", $_[0]);
107 &unlock_file("$backups_dir/$_[0]->{'id'}.backup");
110 =head2 delete_backup(&backup)
112 Deletes the backup whose details are in the given hash ref.
117 &unlink_logged("$backups_dir/$_[0]->{'id'}.backup");
120 =head2 parse_backup_url(string)
122 Converts a URL like ftp:// or a filename into its components. These are
123 user, pass, host, page, port (optional)
128 if ($_[0] && $_[0] =~ /^ftp:\/\/([^:]*):([^\@]*)\@([^\/:]+)(:(\d+))?(\/.*)$/) {
129 return (1, $1, $2, $3, $6, $5);
132 $_[0] =~ /^ssh:\/\/([^:]*):([^\@]*)\@([^\/:]+)(:(\d+))?(\/.*)$/) {
133 return (2, $1, $2, $3, $6, $5);
135 elsif ($_[0] && $_[0] =~ /^upload:(.*)$/) {
136 return (3, undef, undef, undef, $1);
138 elsif ($_[0] && $_[0] =~ /^download:$/) {
139 return (4, undef, undef, undef, undef);
142 return (0, undef, undef, undef, $_[0]);
146 =head2 show_backup_destination(name, value, [local-mode])
148 Returns HTML for a field for selecting a local or FTP file.
151 sub show_backup_destination
153 my ($mode, $user, $pass, $server, $path, $port) = &parse_backup_url($_[1]);
155 $rv .= "<table cellpadding=1 cellspacing=0>";
158 $rv .= "<tr><td>".&ui_oneradio("$_[0]_mode", 0, undef, $mode == 0)."</td>\n";
159 $rv .= "<td colspan=2>$text{'backup_mode0'} ".
160 &ui_textbox("$_[0]_file", $mode == 0 ? $path : "", 40).
161 " ".&file_chooser_button("$_[0]_file")."</td> </tr>\n";
164 $rv .= "<tr><td>".&ui_oneradio("$_[0]_mode", 1, undef, $mode == 1)."</td>\n";
165 $rv .= "<td>$text{'backup_mode1'} ".
166 &ui_textbox("$_[0]_server", $mode == 1 ? $server : undef, 20).
168 $rv .= "<td>$text{'backup_path'} ".
169 &ui_textbox("$_[0]_path", $mode == 1 ? $path : undef, 40).
171 $rv .= "<tr> <td></td>\n";
172 $rv .= "<td>$text{'backup_login'} ".
173 &ui_textbox("$_[0]_user", $mode == 1 ? $user : undef, 15).
175 $rv .= "<td>$text{'backup_pass'} ".
176 &ui_password("$_[0]_pass", $mode == 1 ? $pass : undef, 15).
178 $rv .= "<tr> <td></td>\n";
179 $rv .= "<td>$text{'backup_port'} ".
180 &ui_opt_textbox("$_[0]_port", $mode == 1 ? $port : undef, 5,
181 $text{'default'})."</td> </tr>\n";
184 $rv .= "<tr><td>".&ui_oneradio("$_[0]_mode", 2, undef, $mode == 2)."</td>\n";
185 $rv .= "<td>$text{'backup_mode2'} ".
186 &ui_textbox("$_[0]_sserver", $mode == 2 ? $server : undef, 20).
188 $rv .= "<td>$text{'backup_path'} ".
189 &ui_textbox("$_[0]_spath", $mode == 2 ? $path : undef, 40).
191 $rv .= "<tr> <td></td>\n";
192 $rv .= "<td>$text{'backup_login'} ".
193 &ui_textbox("$_[0]_suser", $mode == 2 ? $user : undef, 15).
195 $rv .= "<td>$text{'backup_pass'} ".
196 &ui_password("$_[0]_spass", $mode == 2 ? $pass : undef, 15).
198 $rv .= "<tr> <td></td>\n";
199 $rv .= "<td>$text{'backup_port'} ".
200 &ui_opt_textbox("$_[0]_sport", $mode == 2 ? $port : undef, 5,
201 $text{'default'})."</td> </tr>\n";
204 # Uploaded file field
205 $rv .= "<tr><td>".&ui_oneradio("$_[0]_mode", 3, undef, $mode == 3).
207 $rv .= "<td colspan=2>$text{'backup_mode3'} ".
208 &ui_upload("$_[0]_upload", 40).
212 # Output to browser option
213 $rv .= "<tr><td>".&ui_oneradio("$_[0]_mode", 4, undef, $mode == 4).
215 $rv .= "<td colspan=2>$text{'backup_mode4'}</td> </tr>\n";
222 =head2 parse_backup_destination(name, &in)
224 Returns a backup destination string, or calls error.
227 sub parse_backup_destination
230 my $mode = $in{"$_[0]_mode"};
233 $in{"$_[0]_file"} =~ /^\/\S/ || &error($text{'backup_edest'});
234 return $in{"$_[0]_file"};
238 &to_ipaddress($in{"$_[0]_server"}) ||
239 &to_ip6address($in{"$_[0]_server"}) ||
240 &error($text{'backup_eserver1'});
241 $in{"$_[0]_path"} =~ /^\/\S/ || &error($text{'backup_epath'});
242 $in{"$_[0]_user"} =~ /^[^:]*$/ || &error($text{'backup_euser'});
243 $in{"$_[0]_pass"} =~ /^[^\@]*$/ || &error($text{'backup_epass'});
244 $in{"$_[0]_port_def"} || $in{"$_[0]_port"} =~ /^\d+$/ ||
245 &error($text{'backup_eport'});
246 return "ftp://".$in{"$_[0]_user"}.":".$in{"$_[0]_pass"}."\@".
248 ($in{"$_[0]_port_def"} ? "" : ":".$in{"$_[0]_port"}).
253 &to_ipaddress($in{"$_[0]_sserver"}) ||
254 &to_ip6address($in{"$_[0]_sserver"}) ||
255 &error($text{'backup_eserver2'});
256 $in{"$_[0]_spath"} =~ /^\/\S/ || &error($text{'backup_epath2'});
257 $in{"$_[0]_suser"} =~ /^[^:]*$/ || &error($text{'backup_euser'});
258 $in{"$_[0]_spass"} =~ /^[^\@]*$/ || &error($text{'backup_epass'});
259 $in{"$_[0]_sport_def"} || $in{"$_[0]_sport"} =~ /^\d+$/ ||
260 &error($text{'backup_esport'});
261 return "ssh://".$in{"$_[0]_suser"}.":".$in{"$_[0]_spass"}."\@".
262 $in{"$_[0]_sserver"}.
263 ($in{"$_[0]_sport_def"} ? "" : ":".$in{"$_[0]_sport"}).
267 # Uploaded file .. save as temp file?
268 $in{"$_[0]_upload"} || &error($text{'backup_eupload'});
269 return "upload:$_[0]_upload";
276 =head2 execute_backup(&modules, dest, &size, &files, include-webmin, exclude-files, &others)
278 Backs up the configuration files for the modules to the selected destination.
279 The backup is simply a tar file of config files. Returns undef on success,
280 or an error message on failure.
285 # Work out modules we can use
287 foreach my $m (@{$_[0]}) {
288 my $mdir = &module_root_directory($m);
289 if ($m && &foreign_check($m) && -r "$mdir/backup_config.pl") {
294 # Work out where to write to
295 my ($mode, $user, $pass, $host, $path, $port) = &parse_backup_url($_[1]);
298 $file = &date_subs($path);
301 $file = &transname();
304 # Get module descriptions
308 my %minfo = &get_module_info($m);
309 $desc{$m} = $minfo{'desc'};
315 # Build list of all files to save from modules
316 foreach my $m (@mods) {
317 &foreign_require($m, "backup_config.pl");
318 my @mfiles = &foreign_call($m, "backup_config_files");
319 push(@files, @mfiles);
320 push(@{$manifestfiles{$m}}, @mfiles);
324 # Add module config files
327 my @cfiles = ( "$config_directory/$m/config" );
328 push(@files, @cfiles);
329 push(@{$manifestfiles{$m}}, @cfiles);
334 foreach my $f (@{$_[6]}) {
337 # A directory .. recursively expand
338 foreach my $sf (&expand_directory($f)) {
341 push(@{$manifestfiles{"other"}}, $sf);
347 push(@{$manifestfiles{"other"}}, $f);
351 # Save the manifest files
352 &execute_command("rm -rf ".quotemeta($manifests_dir));
353 mkdir($manifests_dir, 0755);
355 foreach $m (@mods, "_others") {
356 next if (!defined($manifestfiles{$m}));
357 my $man = "$manifests_dir/$m";
359 &open_tempfile($fh, ">$man");
360 &print_tempfile($fh, map { "$_\n" } @{$manifestfiles{$m}});
361 &close_tempfile($fh);
362 push(@manifests, $man);
365 # Make sure we have something to do
366 @files = grep { $_ && -e $_ } @files;
367 @files || (return $text{'backup_enone'});
370 # Call all module pre functions
373 if (&foreign_defined($m, "pre_backup")) {
374 my $err = &foreign_call($m, "pre_backup", \@files);
376 return &text('backup_epre', $desc{$m}, $err);
382 # Make the tar (possibly .gz) file
383 my $filestemp = &transname();
385 &open_tempfile($fh, ">$filestemp");
386 foreach my $f (&unique(@files), @manifests) {
389 &print_tempfile($fh, $frel."\n");
391 &close_tempfile($fh);
392 my $qfile = quotemeta($file);
394 if (&has_command("gzip")) {
395 &execute_command("cd / ; tar cfT - $filestemp | gzip -c >$qfile",
396 undef, \$out, \$out);
399 &execute_command("cd / ; tar cfT $qfile $filestemp",
400 undef, \$out, \$out);
403 &unlink_file($filestemp);
405 &unlink_file($file) if ($mode != 0);
406 return &text('backup_etar', "<pre>$out</pre>");
408 my @st = stat($file);
409 ${$_[2]} = $st[7] if ($_[2]);
410 @{$_[3]} = &unique(@files) if ($_[3]);
411 &set_ownership_permissions(undef, undef, 0600, $file);
414 # Call all module post functions
416 if (&foreign_defined($m, "post_backup")) {
417 &foreign_call($m, "post_backup", \@files);
423 # FTP upload to destination
425 &ftp_upload($host, &date_subs($path), $file, \$err, undef,
426 $user, $pass, $port);
428 return $err if ($err);
433 &scp_copy($file, "$user\@$host:".&date_subs($path), $pass, \$err,$port);
435 return $err if ($err);
441 =head2 execute_restore(&mods, source, &files, apply, [show-only])
443 Restore configuration files from the specified source for the listed modules.
444 Returns undef on success, or an error message.
449 my ($mods, $src, $files, $apply, $show) = @_;
451 # Fetch file if needed
452 my ($mode, $user, $pass, $host, $path, $port) = &parse_backup_url($src);
458 $file = &transname();
462 &scp_copy("$user\@$host:$path", $file, $pass, \$err, $port);
471 &ftp_download($host, $path, $file, \$err, undef,
472 $user, $pass, $port);
485 my $qfile = quotemeta($file);
486 my $gzipped = ($two eq "\037\213");
490 &has_command("gunzip") || return $text{'backup_egunzip'};
491 $cmd = "gunzip -c $qfile | tar tf -";
494 $cmd = "tar tf $qfile";
497 &execute_command($cmd, undef, \$out, \$out, 0, 1);
499 &unlink_file($file) if ($mode != 0);
500 return &text('backup_euntar', "<pre>$out</pre>");
502 my @tarfiles = map { "/$_" } split(/\r?\n/, $out);
503 my %tarfiles = map { $_, 1 } @tarfiles;
505 # Extract manifests for each module
506 my %hasmod = map { $_, 1 } @{$_[0]};
507 $hasmod{"_others"} = 1;
508 &execute_command("rm -rf ".quotemeta($manifests_dir));
509 my $rel_manifests_dir = $manifests_dir;
510 $rel_manifests_dir =~ s/^\///;
512 &execute_command("cd / ; gunzip -c $qfile | tar xf - $rel_manifests_dir", undef, \$out, \$out);
515 &execute_command("cd / ; tar xf $qfile $rel_manifests_dir", undef, \$out, \$out);
517 opendir(DIR, $manifests_dir);
521 while($m = readdir(DIR)) {
522 next if ($m eq "." || $m eq ".." || !$hasmod{$m});
523 open(MAN, "$manifests_dir/$m");
532 $mfiles{$m} = \@mfiles;
533 push(@files, @mfiles);
537 &unlink_file($file) if ($mode != 0);
538 return $text{'backup_enone2'};
541 # Get descriptions for each module
543 foreach my $m (@{$_[0]}) {
544 my %minfo = &get_module_info($m);
545 $desc{$m} = $minfo{'desc'};
548 # Call module pre functions
549 foreach my $m (@{$_[0]}) {
550 my $mdir = &module_root_directory($m);
551 if ($m && &foreign_check($m) && !$_[4] &&
552 -r "$mdir/backup_config.pl") {
553 &foreign_require($m, "backup_config.pl");
554 if (&foreign_defined($m, "pre_restore")) {
555 my $err = &foreign_call($m, "pre_restore", \@files);
557 &unlink_file($file) if ($mode != 0);
558 return &text('backup_epre2', $desc{$m}, $err);
564 # Lock all files being extracted
567 foreach $f (@files) {
572 # Extract contents (only files specified by manifests)
573 my $flag = $_[4] ? "t" : "x";
574 my $qfiles = join(" ", map { s/^\///; quotemeta($_) } &unique(@files));
576 &execute_command("cd / ; gunzip -c $qfile | tar ${flag}f - $qfiles",
577 undef, \$out, \$out);
580 &execute_command("cd / ; tar ${flag}f $qfile $qfiles",
581 undef, \$out, \$out);
585 # Un-lock all files being extracted
588 foreach $f (@files) {
593 # Check for tar error
595 &unlink_file($file) if ($mode != 0);
596 return &text('backup_euntar', "<pre>$out</pre>");
599 if ($_[3] && !$_[4]) {
600 # Call all module apply functions
601 foreach $m (@{$_[0]}) {
602 if (&foreign_defined($m, "post_restore")) {
603 &foreign_call($m, "post_restore", \@files);
612 =head2 scp_copy(source, dest, password, &error, [port])
614 Copies a file from some source to a destination. One or the other can be
615 a server, like user@foo:/path/to/bar/
620 &foreign_require("proc", "proc-lib.pl");
621 my $cmd = "scp -r ".($_[4] ? "-P $_[4] " : "").
622 quotemeta($_[0])." ".quotemeta($_[1]);
623 my ($fh, $fpid) = &proc::pty_process_exec($cmd);
626 my $rv = &wait_for($fh, "password:", "yes\\/no", ".*\n");
627 $out .= $wait_for_input;
629 syswrite($fh, "$_[2]\n");
632 syswrite($fh, "yes\n");
639 my $got = waitpid($fpid, 0);
640 if ($? || $out =~ /permission\s+denied/i) {
641 ${$_[3]} = "scp failed : <pre>$out</pre>";
645 =head2 find_cron_job(&backup)
647 MISSING DOCUMENTATION
652 my @jobs = &cron::list_cron_jobs();
653 my ($job) = grep { $_->{'user'} eq 'root' &&
654 $_->{'command'} eq "$cron_cmd $_[0]->{'id'}" } @jobs;
658 =head2 nice_dest(destination, [subdates])
660 Returns a backup filename in a human-readable format, with dates substituted.
665 my ($url, $subdates) = @_;
666 my ($mode, $user, $pass, $server, $path, $port) = &parse_backup_url($url);
668 $path = &date_subs($path);
671 return "<tt>$path</tt>";
674 return &text($port ? 'nice_ftpp' : 'nice_ftp',
675 "<tt>$server</tt>", "<tt>$path</tt>",
676 $port ? "<tt>$port</tt>" : "");
679 return &text($port ? 'nice_sshp' : 'nice_ssh',
680 "<tt>$server</tt>", "<tt>$path</tt>",
681 $port ? "<tt>$port</tt>" : "");
684 return $text{'nice_upload'};
687 return $text{'nice_download'};
691 =head2 date_subs(string)
693 Given a string with strftime-style format characters in it like %Y and %S,
694 replaces them with the correct values for the current date and time.
699 if ($config{'date_subs'}) {
701 eval "use posix" if ($@);
702 my @tm = localtime(time());
703 return strftime($_[0], @tm);
710 =head2 show_backup_what(name, webmin?, nofiles?, others)
712 Returns HTML for selecting what gets included in a backup.
717 my ($name, $webmin, $nofiles, $others) = @_;
719 return &ui_checkbox($name."_webmin", 1, $text{'edit_webmin'}, $webmin)."\n".
720 &ui_checkbox($name."_nofiles", 1, $text{'edit_nofiles'}, !$nofiles)."\n".
721 &ui_checkbox($name."_other", 1, $text{'edit_other'}, $others)."<br>".
722 &ui_textarea($name."_files", join("\n", split(/\t+/, $others)), 3, 50);
725 =head2 parse_backup_what(name, &in)
727 Returns the webmin and nofiles flags, and a tab-separated list of other
731 sub parse_backup_what
733 my ($name, $in) = @_;
734 my $webmin = $in->{$name."_webmin"};
735 my $nofiles = !$in->{$name."_nofiles"};
736 $in->{$name."_files"} =~ s/\r//g;
737 my $others = $in->{$name."_other"} ?
738 join("\t", split(/\n+/, $in->{$name."_files"})) : undef;
739 $webmin || !$nofiles || $others || &error($text{'save_ewebmin'});
740 return ($webmin, $nofiles, $others);
743 =head2 expand_directory(directory)
745 Given a directory, return a list of full paths to all files within it.
752 opendir(EXPAND, $dir);
753 my @sf = readdir(EXPAND);
755 foreach my $sf (@sf) {
756 next if ($sf eq "." || $sf eq "..");
757 my $path = "$dir/$sf";
758 if (-l $path || !-d $path) {
762 push(@rv, &expand_directory($path));