575 lines
12 KiB
Perl
575 lines
12 KiB
Perl
|
#----------------------------------------------------------------------
|
||
|
# Copyright 1999-2007 Mitel Networks Corporation
|
||
|
# This program is free software; you can redistribute it and/or
|
||
|
# modify it under the same terms as Perl itself.
|
||
|
#----------------------------------------------------------------------
|
||
|
|
||
|
package esmith::Backup;
|
||
|
|
||
|
use strict;
|
||
|
use warnings;
|
||
|
|
||
|
use File::Copy;
|
||
|
use Unix::PasswdFile;
|
||
|
use Passwd::Unix;
|
||
|
use esmith::lockfile;
|
||
|
|
||
|
use vars qw($VERSION @ISA @EXPORT_OK);
|
||
|
|
||
|
use constant ESMITH_RESTORE_CACHE => '/var/cache/e-smith/restore';
|
||
|
use constant ESMITH_BACKUP_LOCK_FILE => "/var/lock/subsys/backup-running";
|
||
|
|
||
|
@ISA = qw(Exporter);
|
||
|
|
||
|
#path to *.include/*.exclude files
|
||
|
my $BackupDir = '/etc/backup-data.d/';
|
||
|
|
||
|
=head1 NAME
|
||
|
|
||
|
esmith::Backup - interface to server backup/restore information
|
||
|
|
||
|
=head1 SYNOPSIS
|
||
|
|
||
|
# When you want to add or remove files/folders
|
||
|
# in your backup, make files in /etc/backup-data.d
|
||
|
# with one or several path name inside (one per line)
|
||
|
# eg : /opt/myContrib
|
||
|
# /usr/share/myContrib
|
||
|
#
|
||
|
# FileName.include -> add more files/folders
|
||
|
# FileName.exclude -> remove files/folder
|
||
|
|
||
|
use esmith::Backup;
|
||
|
my $backup = new esmith::Backup;
|
||
|
|
||
|
#retrieve the list of your backup exclusions
|
||
|
my @backup_excludes = $backup->excludes;
|
||
|
#or
|
||
|
my @backup_excludes = esmith::Backup->excludes;
|
||
|
|
||
|
# retrieve the list of your backup inclusions
|
||
|
my @backup_list = $backup->restore_list;
|
||
|
#or
|
||
|
my @backup_list = esmith::Backup->restore_list;
|
||
|
|
||
|
=head1 DESCRIPTION
|
||
|
|
||
|
This module provides an abstracted interface to the backup/restore
|
||
|
information
|
||
|
|
||
|
=cut
|
||
|
|
||
|
=begin testing
|
||
|
|
||
|
use esmith::TestUtils qw(scratch_copy);
|
||
|
use_ok("esmith::Backup");
|
||
|
|
||
|
$backup = new esmith::Backup;
|
||
|
isa_ok($backup, 'esmith::Backup');
|
||
|
|
||
|
=end testing
|
||
|
|
||
|
=head2 new
|
||
|
|
||
|
This is the class constructor.
|
||
|
|
||
|
=cut
|
||
|
|
||
|
sub new
|
||
|
{
|
||
|
my $class = ref($_[0]) || $_[0];
|
||
|
my $self = {};
|
||
|
$self = bless $self, $class;
|
||
|
return $self;
|
||
|
}
|
||
|
|
||
|
=head2 restore_list
|
||
|
|
||
|
Returns an (ordered) array of files/directories to recover from the
|
||
|
backup. The pathnames are relative to root.
|
||
|
|
||
|
=cut
|
||
|
|
||
|
sub restore_list
|
||
|
{
|
||
|
my ($self) = @_;
|
||
|
|
||
|
my @backup = (
|
||
|
'home/e-smith',
|
||
|
'etc/e-smith/templates-custom',
|
||
|
'etc/e-smith/templates-user-custom',
|
||
|
'etc/ssh',
|
||
|
'root',
|
||
|
'etc/sudoers',
|
||
|
'etc/passwd',
|
||
|
'etc/shadow',
|
||
|
'etc/group',
|
||
|
'etc/gshadow',
|
||
|
'etc/samba/secrets.tdb',
|
||
|
'etc/samba/smbpasswd',
|
||
|
'etc/samba/schannel_store.tdb',
|
||
|
'etc/backup-data.d',
|
||
|
'var/lib/samba/group_mapping.tdb',
|
||
|
'var/lib/samba/account_policy.tdb',
|
||
|
);
|
||
|
#add all paths in .include files
|
||
|
push @backup, $self->includes;
|
||
|
return @backup;
|
||
|
}
|
||
|
|
||
|
|
||
|
=head2 uniq
|
||
|
|
||
|
Remove all duplicates from the given array.
|
||
|
|
||
|
=cut
|
||
|
|
||
|
sub uniq {
|
||
|
return keys %{{ map { $_ => 1 } @_ }};
|
||
|
}
|
||
|
|
||
|
|
||
|
=head2 load_file_list
|
||
|
|
||
|
head2 load_file_list
|
||
|
Given a file name, return all lines in an array.
|
||
|
|
||
|
=cut
|
||
|
|
||
|
sub load_file_list
|
||
|
{
|
||
|
my ($self, $file) = @_;
|
||
|
my @paths;
|
||
|
open (FILE, $file) or die 'Unable to open the list file: $file';
|
||
|
|
||
|
while (<FILE>) {
|
||
|
|
||
|
#sanitise the line
|
||
|
s/^\s+|\s+$//g;
|
||
|
s/^\/+|\/+$|\n+//g;
|
||
|
s/\/+/\//g;
|
||
|
#we don't want blank line or space
|
||
|
next if /^$|\s+/;
|
||
|
# we don't want some characters
|
||
|
next if /`|'|"|;|&|\||#|\\|:|\*|<|>/;
|
||
|
|
||
|
push(@paths, $_);
|
||
|
}
|
||
|
close(FILE);
|
||
|
|
||
|
return @paths;
|
||
|
}
|
||
|
|
||
|
|
||
|
=head2 load_files_from_dir
|
||
|
|
||
|
Given a directory and an extension, return all lines
|
||
|
from all files using load_file_list function.
|
||
|
|
||
|
=cut
|
||
|
sub load_files_from_dir
|
||
|
{
|
||
|
my ($self, $dir, $extension ) = @_;
|
||
|
my @ret;
|
||
|
my @files = <$dir*.$extension>;
|
||
|
foreach my $file (@files) {
|
||
|
push(@ret,$self->load_file_list($file));
|
||
|
}
|
||
|
return @ret;
|
||
|
}
|
||
|
|
||
|
=head2 includes
|
||
|
|
||
|
Takes a directory as argument.
|
||
|
Returns a list of files from all .include files inside the given directory.
|
||
|
All duplicates are removed.
|
||
|
|
||
|
=cut
|
||
|
|
||
|
sub includes
|
||
|
{
|
||
|
my ($self) = @_;
|
||
|
return uniq($self->load_files_from_dir($BackupDir,'include'));
|
||
|
}
|
||
|
|
||
|
|
||
|
=head2 excludes
|
||
|
|
||
|
Takes a directory as argument.
|
||
|
Returns a list of files from all .exclude files inside the given directory.
|
||
|
All duplicates are removed.
|
||
|
|
||
|
=cut
|
||
|
|
||
|
sub excludes
|
||
|
{
|
||
|
my ($self) = @_;
|
||
|
return uniq($self->load_files_from_dir($BackupDir,'exclude'));
|
||
|
}
|
||
|
|
||
|
|
||
|
=head2 merge_passwd
|
||
|
|
||
|
Merge password files. Takes a filename of a restored password
|
||
|
file and an optional filename for the final merged password file,
|
||
|
defaulting to /etc/passwd
|
||
|
|
||
|
=item *
|
||
|
Save away the recently restored passwd file
|
||
|
|
||
|
=item *
|
||
|
Put the pre-restore passwd file back in place
|
||
|
|
||
|
=item *
|
||
|
Add back any users in the restored passwd file with home
|
||
|
directories under directories which contain user or
|
||
|
machine accounts
|
||
|
|
||
|
=item *
|
||
|
Log any other missing users or UID/GID mismatches
|
||
|
|
||
|
=begin testing
|
||
|
|
||
|
my $installed = '10e-smith-backup/passwd-installed';
|
||
|
my $restored = scratch_copy('10e-smith-backup/passwd-restored');
|
||
|
|
||
|
is($backup->merge_passwd($installed, $restored), 1, 'merge_passwd worked');
|
||
|
|
||
|
use Digest::MD5;
|
||
|
open(FILE, '10e-smith-backup/passwd-merged') || die $!;
|
||
|
my $srcmd5 = Digest::MD5->new->addfile(*FILE)->hexdigest;
|
||
|
open(FILE, $restored) || die $1;
|
||
|
my $destmd5 = Digest::MD5->new->addfile(*FILE)->hexdigest;
|
||
|
close FILE;
|
||
|
|
||
|
is( $srcmd5, $destmd5, 'merge_passwd output looks good' );
|
||
|
|
||
|
=end testing
|
||
|
|
||
|
=cut
|
||
|
|
||
|
my @Scratch_Files = ();
|
||
|
|
||
|
END { unlink @Scratch_Files }
|
||
|
|
||
|
sub merge_passwd
|
||
|
{
|
||
|
my ($self, $pre_restored, $restored) = @_;
|
||
|
|
||
|
$restored ||= '/etc/passwd';
|
||
|
|
||
|
my $tmp = "${restored}.$$";
|
||
|
push @Scratch_Files, $tmp;
|
||
|
copy $restored, $tmp or warn "Couldn't copy $restored, $tmp\n";
|
||
|
copy $pre_restored, $restored or warn "Couldn't copy $pre_restored, $restored\n";
|
||
|
|
||
|
my $merge_from = new Unix::PasswdFile($tmp, rmode => 'r' );
|
||
|
|
||
|
unless ($merge_from)
|
||
|
{
|
||
|
warn "merge_passwd: Couldn't open restored password object\n";
|
||
|
return undef;
|
||
|
}
|
||
|
|
||
|
my $merge_into = new Unix::PasswdFile($restored);
|
||
|
|
||
|
unless ($merge_into)
|
||
|
{
|
||
|
warn "merge_passwd: Couldn't open current password object\n";
|
||
|
return undef;
|
||
|
}
|
||
|
|
||
|
foreach my $user ($merge_from->users)
|
||
|
{
|
||
|
my @details = $merge_into->user($user);
|
||
|
|
||
|
if ( _homedir_ok($merge_from->home($user)) )
|
||
|
{
|
||
|
unless ( defined $details[0] )
|
||
|
{
|
||
|
$merge_into->user($user, $merge_from->user($user));
|
||
|
warn "merge_passwd: Restoring user $user\n";
|
||
|
}
|
||
|
|
||
|
next;
|
||
|
}
|
||
|
|
||
|
unless ( defined $details[0] )
|
||
|
{
|
||
|
warn "merge_passwd: $user - Missing after restore\n";
|
||
|
next;
|
||
|
}
|
||
|
|
||
|
unless ( $merge_into->uid($user) eq $merge_from->uid($user) )
|
||
|
{
|
||
|
warn "merge_passwd: $user - UID changed during restore\n";
|
||
|
next;
|
||
|
}
|
||
|
|
||
|
unless ( $merge_into->gid($user) eq $merge_from->gid($user) )
|
||
|
{
|
||
|
warn "merge_passwd: $user - GID changed during restore\n";
|
||
|
next;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
$merge_into->commit;
|
||
|
|
||
|
return 1;
|
||
|
}
|
||
|
|
||
|
=head2 merge_group
|
||
|
|
||
|
Merge group files. Takes a filename of a restored group
|
||
|
file and an optional filename for the final merged group file,
|
||
|
defaulting to /etc/group.
|
||
|
|
||
|
=item *
|
||
|
Save away the recently restored group file
|
||
|
|
||
|
=item *
|
||
|
Put the pre-restore group file back in place
|
||
|
|
||
|
=item *
|
||
|
Add back any group in the restored group file for which
|
||
|
there are corresponding users with valid home directories.
|
||
|
These users are checked from the passwd file specified in the environment
|
||
|
variable ESMITH_BACKUP_PASSWD_FILE, or /etc/passwd.
|
||
|
|
||
|
=item *
|
||
|
Log any other missing groups or GID mismatches
|
||
|
|
||
|
=item *
|
||
|
Adjust www, admin, shared groups
|
||
|
|
||
|
=begin testing
|
||
|
|
||
|
my $installed = '10e-smith-backup/group-installed';
|
||
|
my $restored = scratch_copy('10e-smith-backup/group-restored');
|
||
|
|
||
|
$ENV{ESMITH_BACKUP_PASSWD_FILE} = '10e-smith-backup/passwd-merged';
|
||
|
is($backup->merge_group($installed, $restored), 1, 'merge_group worked');
|
||
|
|
||
|
use Digest::MD5;
|
||
|
open(FILE, '10e-smith-backup/group-merged') || die $!;
|
||
|
my $srcmd5 = Digest::MD5->new->addfile(*FILE)->hexdigest;
|
||
|
open(FILE, $restored) || die $1;
|
||
|
my $destmd5 = Digest::MD5->new->addfile(*FILE)->hexdigest;
|
||
|
close FILE;
|
||
|
|
||
|
is( $srcmd5, $destmd5, 'merge_group output looks good' );
|
||
|
|
||
|
=end testing
|
||
|
|
||
|
=cut
|
||
|
|
||
|
sub merge_group
|
||
|
{
|
||
|
my ($self, $pre_restored, $restored) = @_;
|
||
|
|
||
|
$restored ||= '/etc/group';
|
||
|
|
||
|
my $tmp = "${restored}.$$";
|
||
|
push @Scratch_Files, $tmp;
|
||
|
copy $restored, $tmp or warn "Couldn't copy $restored, $tmp\n";
|
||
|
copy $pre_restored, $restored or warn "Couldn't copy $pre_restored, $restored\n";
|
||
|
|
||
|
my $merge_from = new Passwd::Unix(group => $tmp);
|
||
|
|
||
|
unless ($merge_from)
|
||
|
{
|
||
|
warn "merge_group: Couldn't open restored group object\n";
|
||
|
return undef;
|
||
|
}
|
||
|
|
||
|
my $merge_into = new Passwd::Unix(group => $restored);
|
||
|
|
||
|
unless ($merge_into)
|
||
|
{
|
||
|
warn "merge_group: Couldn't open current group object\n";
|
||
|
return undef;
|
||
|
}
|
||
|
|
||
|
my $passwd_file = $ENV{ESMITH_BACKUP_PASSWD_FILE} || '/etc/passwd';
|
||
|
|
||
|
my $passwd = new Unix::PasswdFile($passwd_file, rmode => 'r' );
|
||
|
|
||
|
unless ($passwd)
|
||
|
{
|
||
|
warn "merge_group: Couldn't open password object\n";
|
||
|
return undef;
|
||
|
}
|
||
|
|
||
|
foreach my $group ($merge_from->groups)
|
||
|
{
|
||
|
my @details = $merge_into->group($group);
|
||
|
|
||
|
if ( $passwd->user($group) and _homedir_ok($passwd->home($group)) )
|
||
|
{
|
||
|
unless ( defined $details[0] )
|
||
|
{
|
||
|
$merge_into->group($group, $merge_from->group($group));
|
||
|
warn "merge_group: Restoring group $group\n";
|
||
|
}
|
||
|
|
||
|
next;
|
||
|
}
|
||
|
|
||
|
unless ( defined $details[0] )
|
||
|
{
|
||
|
warn "merge_group: $group - Missing after restore\n";
|
||
|
next;
|
||
|
}
|
||
|
|
||
|
my ($merge_into_gid, undef) = $merge_into->group($group);
|
||
|
my ($merge_from_gid, undef) = $merge_from->group($group);
|
||
|
unless ($merge_into_gid eq $merge_from_gid)
|
||
|
{
|
||
|
warn "merge_group: $group - GID changed during restore\n";
|
||
|
next;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
foreach my $special_group ( qw(admin www shared) )
|
||
|
{
|
||
|
$merge_into->group($special_group, $merge_from->group($special_group));
|
||
|
}
|
||
|
|
||
|
return 1;
|
||
|
}
|
||
|
|
||
|
=head2 save_system_files
|
||
|
|
||
|
Save away system files which get cobbered by a restore
|
||
|
|
||
|
=cut
|
||
|
|
||
|
sub save_system_files
|
||
|
{
|
||
|
my ($self) = @_;
|
||
|
|
||
|
my $return = 1;
|
||
|
|
||
|
unless (chdir ESMITH_RESTORE_CACHE)
|
||
|
{
|
||
|
warn "Couldn't change to cache directory\n";
|
||
|
return undef;
|
||
|
}
|
||
|
|
||
|
foreach my $file ( $self->restore_list )
|
||
|
{
|
||
|
if ( -f "/$file" )
|
||
|
{
|
||
|
unless (copy "/$file", "./$file")
|
||
|
{
|
||
|
warn "Couldn't copy /$file to ./$file\n";
|
||
|
$return = undef;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return $return;
|
||
|
}
|
||
|
|
||
|
=head2 merge_system_files
|
||
|
|
||
|
Merge restored system files with ones on the system
|
||
|
|
||
|
=cut
|
||
|
|
||
|
sub merge_system_files
|
||
|
{
|
||
|
my ($self) = @_;
|
||
|
|
||
|
unless (chdir ESMITH_RESTORE_CACHE)
|
||
|
{
|
||
|
warn "Couldn't change to cache directory\n";
|
||
|
return undef;
|
||
|
}
|
||
|
|
||
|
if ( -f "./etc/passwd" and -f "/etc/passwd" )
|
||
|
{
|
||
|
$self->merge_passwd( "./etc/passwd", "/etc/passwd" );
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
warn "Skipping password file merge\n";
|
||
|
}
|
||
|
|
||
|
if ( -f "./etc/group" and -f "/etc/group" )
|
||
|
{
|
||
|
$self->merge_group( "./etc/group", "/etc/group" );
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
warn "Skipping group file merge\n";
|
||
|
}
|
||
|
|
||
|
my $now = time();
|
||
|
|
||
|
foreach my $file ( $self->restore_list )
|
||
|
{
|
||
|
if ( -f "./$file" )
|
||
|
{
|
||
|
warn "Preserving $file as $file.$now\n";
|
||
|
|
||
|
rename "./$file", "./$file.$now"
|
||
|
or warn "Couldn't rename ./$file, ./$file.$now\n";
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return 1;
|
||
|
}
|
||
|
|
||
|
|
||
|
=head2 _homedir_ok
|
||
|
|
||
|
Returns true if the given directory is one we want to
|
||
|
restore: /home/e-smith for user accounts or
|
||
|
/noexistingpath for machine accounts
|
||
|
|
||
|
=cut
|
||
|
|
||
|
sub _homedir_ok
|
||
|
{
|
||
|
my $dir = shift or return;
|
||
|
|
||
|
return $dir =~ m:^/(home/e-smith|noexistingpath): ;
|
||
|
}
|
||
|
|
||
|
=head2
|
||
|
|
||
|
set_lock - set lock before running backup
|
||
|
see bug #9217
|
||
|
|
||
|
=cut
|
||
|
|
||
|
sub set_lock
|
||
|
{
|
||
|
return esmith::lockfile::LockFileOrReturn(ESMITH_BACKUP_LOCK_FILE);
|
||
|
}
|
||
|
|
||
|
|
||
|
=head2
|
||
|
|
||
|
remove_lock - remove lock after running backup
|
||
|
|
||
|
=cut
|
||
|
|
||
|
sub remove_lock
|
||
|
{
|
||
|
esmith::lockfile::UnlockFile(shift);
|
||
|
}
|
||
|
|
||
|
|
||
|
|
||
|
=head1 AUTHOR
|
||
|
|
||
|
SME Server Developers <bugs@e-smith.com>
|
||
|
|
||
|
=head1 SEE ALSO
|
||
|
|
||
|
=cut
|
||
|
|
||
|
1;
|