smeserver-backup/root/usr/share/perl5/vendor_perl/esmith/Backup.pm

575 lines
12 KiB
Perl
Raw Permalink Normal View History

#----------------------------------------------------------------------
# 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;