#---------------------------------------------------------------------- # 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 () { #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 =head1 SEE ALSO =cut 1;