initial commit of file from CVS for e-smith-backup on Thu 26 Oct 11:24:24 BST 2023

This commit is contained in:
2023-10-26 11:24:24 +01:00
parent bb6b15a5a9
commit fe41ccadec
81 changed files with 9900 additions and 2 deletions

View File

@@ -0,0 +1,574 @@
#----------------------------------------------------------------------
# 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;

View File

@@ -0,0 +1,94 @@
#----------------------------------------------------------------------
# Copyright 1999-2003 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::BackupHistoryDB;
use strict;
use warnings;
use esmith::DB::db;
our @ISA = qw( esmith::DB::db );
=head1 NAME
esmith::BackupHistoryDB - interface to esmith backup history database
=head1 SYNOPSIS
use esmith::BackupHistoryDB;
my $c = esmith::BackupHistoryDB->open;
# everything else works just like esmith::DB::db
=head1 DESCRIPTION
This module provides an abstracted interface to the esmith master
configuration database.
Unless otherwise noted, esmith::BackupHistoryDB acts like esmith::DB::db.
=cut
=head2 open()
Like esmith::DB->open, but if given no $file it will try to open the
file in the ESMITH_BACKUPHISTORY_DB environment variable or "backups" in
the default database directory.
=begin testing
use_ok("esmith::BackupHistoryDB");
$C = esmith::BackupHistoryDB->open('10e-smith-backup/backuphistory.conf');
isa_ok($C, 'esmith::BackupHistoryDB');
my $rec = $C->get('1053551285');
isa_ok($rec, 'esmith::DB::Record');
is( $rec->prop('EndEpochTime'), 1053551299,
"We can get stuff from the db");
=end testing
=cut
sub open {
my($class, $file) = @_;
$file = $file || $ENV{ESMITH_BACKUPHISTORY_DB} || "backups";
return $class->SUPER::open($file) || $class->SUPER::create($file);
}
=head2 open_ro()
Like esmith::DB->open_ro, but if given no $file it will try to open the
file in the ESMITH_BACKUPHISTORY_DB environment variable or "backups" in
the default database directory.
=begin testing
=end testing
=cut
sub open_ro {
my($class, $file) = @_;
$file = $file || $ENV{ESMITH_BACKUPHISTORY_DB} || "backups";
return $class->SUPER::open_ro($file);
}
=head1 AUTHOR
SME Server Developers <bugs@e-smith.com>
=head1 SEE ALSO
L<esmith::DB::db>
L<esmith::DB::Record>
=cut
1;

View File

@@ -0,0 +1,510 @@
#----------------------------------------------------------------------
# Copyright 2015 Ian Wells
# This program is free software; you can redistribute it and/or
# modify it under the same terms as Perl itself.
#----------------------------------------------------------------------
package esmith::BlockDevices;
use strict;
use warnings;
use English '-no_match_vars';
use Carp;
use File::Path qw(make_path remove_tree);
use POSIX qw(:sys_wait_h strftime);
use Locale::gettext;
use File::stat;
use v5.10.1;
use Taint::Util;
use Readonly;
use File::Find;
use vars qw($VERSION @ISA @EXPORT_OK);
@ISA = qw(Exporter);
=head1 NAME
esmith::BlockDevices - Module to handle block devices
=head1 SYNOPSIS
use esmith::BlockDevices;
my $devices = BlockDevices->new ();
=head1 DESCRIPTION
This module provides an abstracted interface to the
block devices used for backup/restore
=cut
my $EMPTY = q{};
sub new
{
my $class = shift;
my $self = {
_blox => lsblk(),
mount => findValidFS(),
_fstype => $EMPTY,
allowmount => $EMPTY,
@_,
};
bless $self, $class;
return $self;
}
sub lsblk
{
#ToDo add some comments
my %blox; # a hash to hold the device information
my $short = qx(/bin/lsblk -sdn -o KNAME);
my @long = qx(/bin/lsblk -P -b -o KNAME,MAJ:MIN,RM,RO,TYPE,MOUNTPOINT,FSTYPE,LABEL,UUID,MODEL,SIZE,STATE,MODE,OWNER,GROUP);
# Not all of this information may be needed currently, but it does not affect the processing time
untaint ($short);
untaint (@long);
my $devicemodel= $EMPTY;
for (@long)
{
my @line = split /\"\s/s;
my $name;
if ($line[0] =~ /KNAME=\"(.*)/s)
{
$name = $1;
}
else {carp 'Could not match KNAME'; last;} # should never occur.
$blox{$name}{tip} = ($short =~ m/^$name$/sm) ? 1 : 0;
for (@line)
{
my ($key,$value) = split /=/s;
$value =~ s/\"//sg;
$blox{$name}{$key} = trim($value);
}
if ($blox{$name}{TYPE} =~ /rom|disk/s)
{
$devicemodel = $blox{$name}{MODEL};
}
else
{
$blox{$name}{MODEL} = trim($devicemodel);
}
$blox{$name}{SIZEH} = scaleIt($blox{$name}{SIZE});
}
return \%blox;
}
sub findValidFS
{
# Find all filesystem types that are supported
my %fs; # a hash to hold the supported filesystem type information
my @cmd = `cat /proc/filesystems`;
foreach (@cmd)
{
if (/.*\t(.*?)$/s){$fs {$1}=$1;}
}
@cmd = `ls -1 /lib/modules/\$(uname -r)/kernel/fs/*/*ko.*`;
foreach (@cmd)
{
if (/.*\/(.*?)\.ko/s){$fs {$1}=$1;}
}
# If ext4 driver is present, add ext2 and ext3
if(exists($fs{ext4}))
{
$fs{'ext2'}='ext2';
$fs{'ext3'}='ext3';
}
return \%fs;
}
sub scanBlocks
{
# Scan all the block devices
# This takes some seconds on systems with many filesystems
my ($self) = @_;
$self->{_blox} = lsblk;
$self->{_fstype} = findValidFS;
return;
}
sub list
{
my ($self) = @_;
my @dirs=();
my $hashref = $self->{_blox};
foreach my $drive (keys %{$hashref})
{
push @dirs, $drive;
}
return @dirs;
}
sub checkBackupDriveSize
{
my ($self,$drive, $size) = @_;
my $hashref = $self->{_blox};
my $sz = $EMPTY;
my $mntdir = $self->{mount};
Readonly my $VFAT_LIMIT => 2147483648;
Readonly my $KBYTE => 1024;
# size > drive size
if ($size > $hashref->{$drive}{SIZE})
{
return 1; # filesystem too small
}
# FAT32 drive and size > 2G
if (($size > $VFAT_LIMIT) && ($hashref->{$drive}{FSTYPE} eq 'vfat'))
{
return 2; # filesystem vfat limit
}
#ToDo add a check here to see if mounting is allowed by db value
# check mount and find actual size
if ($self->mountable ($drive)) # Only check filesystems that appear mountable
{
$self->mount ($drive);
my $filesize = -s "$mntdir/smeserver.tgz";
# Check free disk space
my $df = qx(/usr/bin/df -P \"$mntdir\");
if ($df =~ /(\S+)\s+(\S+)\s+(\S+)\s+(\d*%)/s)
{
my $dsize = ($3 * $KBYTE) + ($filesize //= 0);
if ($size > $dsize) # not enough space
{
$sz = 3; # filesystem has too little free space
}
}
else # fail (never seen in testing)
{
$sz = 4; # Failed to get disk size
}
$self->unmount;
}
return $sz;
}
# Check each block device
# Return two arrays, valid drives, invalid drives
sub checkBackupDrives
{
my ($self,$bsize) = @_;
my @valid = ();
my @invalid = ();
$self->scanBlocks; # scan all block devices
my $hashref = $self->{_blox};
my $allowmount = $self->{'allowmount'}; # Are mounted drives allowed in $checks.
my $checks = 'UU RO FS'; # These checks are always valid
$checks .= ' MO' if ($allowmount eq 'enabled');
$checks .= ' SZ' if ($bsize); # Only run the size check when a valid size is given
foreach my $drive (keys %{$hashref})
{
$hashref->{$drive}{REASON} = $EMPTY; # Reason for a filesystem being (in)valid
next unless $hashref->{$drive}{tip}; #Ignore drives that have child filesystems
# drives mounted on /, /boot, or [SWAP] are never valid for backups
next if ($hashref->{$drive}{MOUNTPOINT} =~ /^\/boot$|^\[SWAP\]$|^\/$/s );
# validate each filesystem against the checks
foreach my $check (split / /s, $checks)
{
for ($check)
{
if (/^UU/si) # No UUID
{
$hashref->{$drive}{REASON} .='UU ' unless $self->uuid ($drive); last;
}
if (/^RO/si) # Read Only
{
$hashref->{$drive}{REASON} .='RO ' if $self->readonly ($drive); last;
}
if (/^FS/si) # Invalid filesystem
{
$hashref->{$drive}{REASON} .='FS ' unless $self->validFS ($drive); last;
}
if (/^MO/si) # Mounted
{
$hashref->{$drive}{REASON} .='MO ' if $self->mountpoint ($drive); last;
}
if (/^SZ/si) # filesystem size, this includes mounting to check free space
{
$hashref->{$drive}{REASON} .='SZ ' if $self->checkBackupDriveSize ($drive, $bsize);
#ToDo the return value contains the reason why there is insufficient space, but this is not used yet.
last;
}
{ carp "not supported yet in checkBackupDrives: $check"; } # Should never be seen
}
}
if ($hashref->{$drive}{REASON})
{
push @invalid, $drive;
}
else
{
push @valid, $drive;
}
}
return (\@valid, \@invalid);
}
sub findBackup
{
my ($self, $kname, $foundref, $maxDepth, $count) = @_;
my $hashref = $self->{_blox};
my $mountpoint = $self->{'mount'};
my $file = 'smeserver.tgz';
$self->mount ($kname);
sleep 1;
# start with the absolute path
my $findRoot = Cwd::realpath($mountpoint);
# determine the depth of our beginning directory
my $begDepth = 1 + grep { length } File::Spec->splitdir($findRoot);
find (
{
preprocess => sub
{ @_ if (scalar File::Spec->splitdir($File::Find::dir) - $begDepth) <= $maxDepth },
wanted => sub
{
if (($_ =~ m/^$file/s) && ($File::Find::name =~ qr|^([-+@\w\s./:\\]+)$| )) # if matching the backup name
{
$$count++;
my $sb = stat $1;
${$foundref}{$$count}{count}=$$count;
${$foundref}{$$count}{device}=$kname;
${$foundref}{$$count}{path} = $1;
${$foundref}{$$count}{path} =~ s/$mountpoint//; #strip off the mountpoint
${$foundref}{$$count}{path} =~ s/$file//; #strip off the filename
${$foundref}{$$count}{size}=$sb->size; # size in bytes
${$foundref}{$$count}{sizeH}=scaleIt($sb->size); # human readable size
${$foundref}{$$count}{time}=strftime '%d %b %g %H:%M', localtime $sb->mtime;
}
},
untaint => 1,
untaint_pattern => qr|^([-+@\w\s./:\\]+)$|,
untaint_skip =>1,
},
$findRoot
);
$self->unmount;
return;
}
sub desc # brief description of a filesystem
{
my ($self,$kname) = @_;
my $hashref = $self->{_blox};
my $model = $hashref->{$kname}{MODEL};
my $label = $hashref->{$kname}{LABEL} || gettext('no label');
my $size = $hashref->{$kname}{SIZEH};
return "$label $model $size";
}
# Given the KNAME check if the filesystem.could be mountable
# Check that there are no children, i.e. a tip
# Check that it has a UUID, Filesystem,
sub mountable
{
my ($self,$kname) = @_;
my $hashref = $self->{_blox};
return ($hashref->{$kname}{tip} && $hashref->{$kname}{UUID} && _isFS ($hashref->{$kname}{FSTYPE})) ? 1 : $EMPTY;
}
# Given the KNAME check if the filesystem.is read-only
# returns 1 for Read-Only and $EMPTY for R-W
sub readonly
{
my ($self,$kname) = @_;
my $hashref = $self->{_blox};
return ($hashref->{$kname}{RO}) ? 1 : $EMPTY;
}
sub mountpoint
{
my ($self,$kname) = @_;
my $hashref = $self->{_blox};
return ($hashref->{$kname}{MOUNTPOINT});
}
sub uuid
{
my ($self,$kname) = @_;
my $hashref = $self->{_blox};
return ($hashref->{$kname}{UUID});
}
sub model
{
my ($self,$kname) = @_;
my $hashref = $self->{_blox};
return ($hashref->{$kname}{MODEL});
}
# Given the KNAME return the label
# returns 'no label' if none found
sub label
{
my ($self,$kname) = @_;
my $hashref = $self->{_blox};
return ($hashref->{$kname}{LABEL}) || gettext('no label');
}
sub size
{
my ($self,$kname) = @_;
my $hashref = $self->{_blox};
return ($hashref->{$kname}{SIZE});
}
# Given a filesystem.(eg sr0) check if it's filesystem type is allowed
sub validFS
{
my ($self,$kname) = @_;
my $hashref = $self->{_blox};
my $fsref = $self->{_fstype};
return ($fsref->{$hashref->{$kname}{FSTYPE}}) || $EMPTY;
}
# Given a filesystem.type (eg vfat) check if it is allowed
sub _isFS
{
my ($filesystem) = @_;
return $EMPTY unless $filesystem;
my $fsref = findValidFS;
return ($fsref->{$filesystem}) || $EMPTY;
}
# Return the reason string which indicates why a drive is (in)valid
sub reason
{
my ($self,$kname) = @_;
my $hashref = $self->{_blox};
return ($hashref->{$kname}{REASON});
}
# Given the KNAME mount the filesystem, example
# system ('/bin/mount', '-t', 'vfat', '-U', '9891-4C8A', '/tmp/mnt');
sub mount
{
my ($self, $kname) = @_;
my $hashref = $self->{_blox};
$self->createMountpoint;
system ('/bin/mount', '-t', $hashref->{$kname}{FSTYPE}, '-U', $hashref->{$kname}{UUID}, $self->{mount}) == 0
or croak (gettext('Failed to mount')." $self->{mount},$hashref->{$kname}{FSTYPE},$hashref->{$kname}{UUID}: $?");
return;
}
# Unmount the block device
sub unmount
{
my $self = shift;
system('/bin/umount', $self->{mount}) == 0
or croak (gettext('Failed to unmount')." $self->{mount}: $?");
return;
}
# Create the mountpoint directory
# Error if already mounted
sub createMountpoint
{
my $self = shift;
my $mount = $self->{mount};
# Check if the mountpoint is in use
if (!checkMount ($mount))
{
# Try to unmount, will die if fails
$self->unmount;
}
if ($mount && ! -d $mount)
{
eval {make_path($mount)};
croak (gettext('Error while creating')." $mount $EVAL_ERROR".gettext('Maybe insufficient permissions.')) if $EVAL_ERROR;
}
return;
}
sub destroy
{
# cleanup, unmount and remove mountpoint
my $self = shift;
my $mount = $self->{mount};
# Check if the mountpoint is in use
if (!checkMount ($mount))
{
$self->unmount;
}
if ($mount && -d $mount)
{
eval {remove_tree($mount)};
croak (gettext('Error while deleting')." $mount $EVAL_ERROR") if $EVAL_ERROR;
}
return;
}
### The following subroutines are not specific to block devices
sub scaleIt {
Readonly my $KBYTE => 1024;
my( $size, $n ) =( shift, 0 );
++$n and $size /= $KBYTE until $size < $KBYTE;
if ($size >= 1000){++$n ; $size /= $KBYTE;}
return sprintf "%.3g %s",
$size, ( qw[ bytes KB MB GB TB] )[ $n ];
}
sub checkMount
{
# check if $mountdir is mounted
my $mountdir = shift;
$|=1; # Auto-flush
# copy STDOUT to another filehandle
open (my $STDOLD, '>&', STDOUT);
open(STDOUT, ">/dev/null");
if ( open(MOUNTDIR, "|-", "/bin/findmnt", $mountdir)){;}
# restore STDOUT
open (STDOUT, '>&', $STDOLD);
return (!close(MOUNTDIR));
}
# remove leading and trailing spaces from a string
# this should be moved to a util library.
sub trim
{
my ($string) = @_;
$string =~ s/^\s+|\s+$//g;
return $string;
}
1;

View File

@@ -0,0 +1,61 @@
package esmith::console::backup_running;
use strict;
use warnings;
use esmith::ConfigDB;
use Locale::gettext;
sub new
{
my $class = shift;
my $self = {};
bless $self, $class;
return $self;
}
sub doit
{
my ($self, $console, $db) = @_;
#-------------------------------------------------------------
# check whether a backup in process and incomplete
#-------------------------------------------------------------
my $restore_db = esmith::ConfigDB->open_ro("/etc/e-smith/restore");
return unless $restore_db;
my $restore_state = $restore_db->get_prop('restore', 'state') || 'idle';
return unless ($restore_state eq 'running');
my ($rc, $choice) = $console->message_page
(
title => gettext("Inconsistent system state"),
text =>
gettext("********** Inconsistent system state detected ***********") .
"\n\n" .
gettext("The restoration of a system backup was running and incomplete at the time of the last reboot. The system should not be used in this state.") .
"\n\n" .
gettext("Consult the User Guide for further instructions."),
);
($rc, $choice) = $console->yesno_page
(
title => gettext("System will be halted"),
text =>
gettext("The server will now be halted.") .
"\n\n" .
gettext("Consult the User Guide for recovery instructions.") .
"\n\n" .
gettext("Do you wish to halt the system right now?"),
);
return unless ($rc == 0);
system("/usr/bin/tput", "clear");
system("/sbin/e-smith/signal-event", "halt");
# A bit of a hack to avoid the console restarting before the
# reboot takes effect.
sleep(600);
}
1;

View File

@@ -0,0 +1,294 @@
package esmith::console::perform_backup;
use strict;
use warnings;
use esmith::ConfigDB;
use esmith::console;
use esmith::util;
use Locale::gettext;
use esmith::Backup;
use Carp;
use feature qw( say );
use esmith::BlockDevices;
use POSIX qw(:sys_wait_h strftime);
use File::stat;
use Taint::Util;
my $EMPTY = q{};
# lock file.. see bug 9127
my $backup_lock;
sub new
{
my $class = shift;
my $self = {
name => gettext("Perform backup to removable media"),
order => 80,
};
bless $self, $class;
return $self;
}
sub name
{
return $_[0]->{name};
}
sub order
{
return $_[0]->{order};
}
sub backup_size
{
my $self = shift;
}
# subs to set and remove lock on backup
sub SetLock
{
print "Setting backup lock file\n";
$backup_lock = esmith::Backup::set_lock;
return $backup_lock;
}
sub RemoveLock
{
if (defined($backup_lock))
{
print "Removing backup lock file\n";
esmith::Backup::remove_lock($backup_lock);
$backup_lock = undef;
}
}
sub make_backup_callback
{
my ($device, $CompressionLevel) = @_;
return sub {
my $fh = shift;
my @backup_list = esmith::Backup->restore_list;
my @backup_excludes = esmith::Backup->excludes;
my $backup_size = backupSize (@backup_list);
# set lock.. if not, exit
unless (SetLock()) {
die "Error: failed to create lock file.. is a backup already running?";
}
open(OLDSTDOUT, ">&STDOUT");
unless (open(STDOUT, ">$device/smeserver.tgz"))
{
return gettext("Could not create backup file on device").": $!\n";
}
open(OLDSTDERR, ">&STDERR");
my $logger = open(STDERR, "|-");
die "Can't fork: $!\n" unless defined $logger;
unless ($logger)
{
exec qw(/usr/bin/logger -p local1.info -t console_backup);
}
my $status = 0;
my $gzip = open(GZIP, "|-");
return "could not run gzip" unless defined $gzip;
unless ($gzip)
{
close $fh;
exec "gzip", $CompressionLevel;
}
my $pv = open(PV, "|-");
return "could not run pv" unless defined $pv;
unless ($pv)
{
open(STDOUT, ">&GZIP");
close GZIP;
open(STDERR, ">&$fh");
exec qw(pv -i 0.2 -n -s), $backup_size
}
my $tar = fork;
return "could not run tar" unless defined $tar;
unless ($tar)
{
open(STDOUT, ">&PV");
close PV;
close GZIP;
close $fh;
chdir "/";
#Create the archive
my @directories = grep { -e $_ } @backup_list;
my @exclude = map ("--exclude=$_",@backup_excludes);
exec ("/bin/tar cf - @directories @exclude");
}
waitpid($tar, 0);
warn "status from tar was $?\n" if $?;
unless (close PV)
{
$status |= $! ? $! : $?;
warn "status from pv is $status\n" if $status;
}
unless (close GZIP)
{
$status |= $! ? $! : $?;
warn "status from gzip is $status\n" if $status;
}
open(STDOUT, ">&OLDSTDOUT");
open(STDERR, ">&OLDSTDERR");
close(OLDSTDERR);
close(OLDSTDOUT);
RemoveLock();
return $status ? gettext("Backup failed. Look at the log files for more details.") : gettext("Backup successfully created.");
};
}
sub doit
{
my ($self, $console, $db) = @_;
my @backup_list = esmith::Backup->restore_list;
my @backup_excludes = esmith::Backup->excludes;
my $compressionLevel = $db->get_prop('backupconsole', 'CompressionLevel') || '-6';
my $mountpoint = $db->get_prop('backupconsole', 'Mountpoint') || '/mnt/bootstrap-console-backup';
my $allowMounted = $db->get_prop('backupconsole', 'AllowMounted') || 'disabled'; ### For future use
$ENV{PATH} = "/bin:/usr/bin";
$ENV{HOME} = "/root";
my $devices = esmith::BlockDevices->new ('mount' => $mountpoint, 'allowmount' => $allowMounted);
INITIATE_BACKUP:
my ($rc, $choice) = $console->yesno_page
(
title => gettext("Create Backup to removable media"),
defaultno => 1,
text => gettext('Do you wish to create a backup on removable media?')."\n\n".
gettext('Insert removable media before proceeding.')."\n".
gettext('It may take many seconds to scan for media.'),
);
if ($rc != 0)
{
$devices->destroy;
return;
}
### determine which filesystems are valid or not for backups
# check expected backup size
my $backup_size = backupSize (@backup_list);
# validate each filesystem
my ($valid, $invalid) = $devices->checkBackupDrives ($backup_size);
my $text = $EMPTY;
if (${$invalid}[0]) # If there are filesystems that are not valid.
{
$text .= gettext ('These filesystems are not valid:')."\n";
foreach my $drive (sort @{$invalid})
{
$text .= "$drive ".$devices->desc($drive).' '.gettext ('Reason').': '.$devices->reason($drive)."\n";
}
$text .= "\n";
}
unless (${$valid}[0]) # Unless a device is found show error page
{
my $title = gettext('No valid backup device found').' '.gettext('size').' '.esmith::BlockDevices::scaleIt($backup_size);
$text .= "\n$title, ".gettext('please try again');
($rc, $choice) = $console->yesno_page
(
title => $title,
text => $text,
left => gettext('Try again'),
right => gettext('Cancel'),
);
if ($rc == 0) # Try Again
{
goto INITIATE_BACKUP;
}
else
{
$devices->destroy;
return;
}
}
$text .= gettext ('The following are valid for backup').' (';
$text .= gettext ('size').' '.esmith::BlockDevices::scaleIt($backup_size).')';
#ToDo when valid + invalid > 13 then may need to limit the information
my @args = map { $_ => $devices->desc($_) } @{$valid};
# Display the available backup destinations.
($rc, $choice) = $console->menu_page
(
title => gettext('Choose device to use for backup').' '.gettext('size').' '.esmith::BlockDevices::scaleIt($backup_size),
text => $text,
argsref => \@args,
left => gettext('Cancel'),
right => gettext('OK'),
);
goto INITIATE_BACKUP unless ($rc == 0);
untaint $choice;
$devices->mount ($choice); # mount the chosen filesystem
if (@backup_excludes) {
my $backupexclude = join ("\n/", sort @backup_excludes);
($rc, $choice) = $console->yesno_page
(
title => gettext("Some parts are excluded of your backup"),
left => gettext("Next"),
right => gettext("Cancel"),
text => '/' . $backupexclude,
);
return unless $rc == 0;
}
$console->infobox(
title => gettext("Preparing for backup"),
text => gettext("Please stand by while the system is prepared for backup..."),
);
system("/sbin/e-smith/signal-event", "pre-backup");
$console->gauge(make_backup_callback($mountpoint,$compressionLevel), 'title' => gettext('Creating backup file'));
$devices->destroy;
system("/sbin/e-smith/signal-event", 'post-backup');
RemoveLock();
$console->message_page
(
title => gettext('Backup complete'),
text => gettext('Remove backup media.'),
);
return;
}
sub backupSize
{
my $size;
unless (open(DU, "-|"))
{
open(STDERR, ">/dev/null");
exec qw(/usr/bin/du -sb), map { "/$_" } @_;
}
while (<DU>)
{
next unless (/^(\d+)/);
$size += $1;
}
close DU;
return $size;
}
#use esmith::console;
#esmith::console::perform_backup->new->doit(esmith::console->new,
# esmith::ConfigDB->open);
1;

View File

@@ -0,0 +1,340 @@
package esmith::console::perform_restore;
use strict;
use warnings;
use esmith::ConfigDB;
use esmith::console;
use Locale::gettext;
use Carp;
use feature qw( say );
use esmith::BlockDevices;
use Taint::Util;
use esmith::Backup;
use Data::UUID;
my $EMPTY = q{};
my $backup_lock;
sub new
{
my $class = shift;
my $self = {
name => gettext("Restore from removable media"),
order => installOrder(),
bootstrap => 0,
@_,
};
bless $self, $class;
return $self;
}
sub name
{
return $_[0]->{name};
}
sub order
{
return $_[0]->{order};
}
sub SetLock
{
print "Setting backup lock file\n";
$backup_lock = esmith::Backup::set_lock;
return $backup_lock;
}
sub RemoveLock
{
if (defined($backup_lock))
{
print "Removing backup lock file\n";
esmith::Backup::remove_lock($backup_lock);
$backup_lock = undef;
}
}
sub new_restore_id
{
my $ug = Data::UUID->new;
my $uid = $ug->create_str();
return $uid;
}
sub make_restore_callback
{
my ($mountpoint, $restoreid) = @_;
return sub {
my $fh = shift;
# Check no other backups or restores are in progress
unless (SetLock()) {
die "Error: failed to create lock file. Is a backup or restore already running?";
}
open(OLDSTDOUT, ">&STDOUT");
my $status = 0;
chdir "/";
open(OLDSTDERR, ">&STDERR");
my $logger = open(STDERR, "|-");
die "Can't fork: $!\n" unless defined $logger;
unless ($logger)
{
exec qw(/usr/bin/logger -p local1.info -t console_restore);
}
my $tar = open(TAR, "|-");
return "could not run tar" unless defined $tar;
unless($tar)
{
exec "tar xf -";
}
my $gunzip = open(GUNZIP, "|-");
return "could not run gunzip" unless defined $gunzip;
unless($gunzip)
{
open(STDOUT, ">&TAR");
close TAR;
exec "gunzip";
}
my $pv = fork;
return "could not run pv" unless defined $pv;
unless ($pv)
{
open(STDOUT, ">&GUNZIP");
open(STDERR, ">&$fh");
close GUNZIP;
close TAR;
exec "pv -n $mountpoint/smeserver.tgz";
}
waitpid($pv,0);
warn "status from pv was $?\n" if $?;
unless(close GUNZIP)
{
$status |= $! ? $! : $?;
warn "status from gunzip is $status\n" if $status;
}
unless(close TAR)
{
$status |= $! ? $! : $?;
warn "status from tar is $status\n" if $status;
}
open(STDOUT, ">&OLDSTDOUT");
open(STDERR, ">&OLDSTDERR");
close(OLDSTDOUT);
close(OLDSTDERR);
# Unlock and check for success or failure before returning
RemoveLock();
if ($status) {
return gettext("Restore failed. Your system is likely in an inconsistent state - look at the log files for more details.");
} else {
system("touch", "/tmp/restore_$restoreid");
return gettext("Restore successful.");
}
}
}
sub doit
{
my ($self, $console, $db) = @_;
my $compressionLevel = $db->get_prop('backupconsole', 'CompressionLevel') || '-6';
my $mountpoint = $db->get_prop('backupconsole', 'Mountpoint') || '/mnt/bootstrap-console-backup';
my $allowMounted = $db->get_prop('backupconsole', 'AllowMounted') || 'disabled'; ### For future use
my $restoreMaxDepth = $db->get_prop('backupconsole', 'MaxDepth') || 1; ### For future use
my %found;
my $backupcount = 0;
my $backupdrive; # Which filesystem holds the backup
my $backupfile; # full path to the backup
my ($time, $size); # time and size of chosen backup
return if (($db->get_prop('bootstrap-console', 'Run') eq 'no') && $self->{bootstrap} ); # regular reboot
if ($db->get_prop('bootstrap-console', 'Run') eq 'yes') # called from bootstrap console
{
return if ($db->get_value('PasswordSet') eq 'yes'); # too late
}
return if ($db->get_prop('bootstrap-console', 'Restore') eq 'disabled');
my $devices = esmith::BlockDevices->new ('mount' => $mountpoint, 'allowmount' => $allowMounted);
INITIATE_RESTORE:
my ($rc, $choice) = $console->yesno_page
(
title => gettext("Restore From Backup"),
defaultno => 1,
text => gettext('Do you wish to restore from backup?')."\n\n".
gettext('Insert removable media before proceeding.')."\n".
gettext('It may take many seconds to scan for media.'),
); # Buttons are Yes & No
if ($rc != 0) # choice was not Yes
{
$devices->destroy;
return;
}
### determine which filesystems are valid or not for backups
# validate each filesystem
my ($valid, $invalid) = $devices->checkBackupDrives ($EMPTY);
my $text = $EMPTY;
if (${$valid}[0]) # There are filesystems that could hold a backup.
{
$text .= gettext ('These filesystems could hold backups')."\n";
foreach my $drive (sort @{$valid})
{
$text .= "$drive ".$devices->desc($drive)."\n";
$devices->findBackup ($drive, \%found, $restoreMaxDepth, \$backupcount);
}
$text .= "\n";
}
unless ($backupcount) # Unless a valid backup is found show error page
{
if (${$invalid}[0]) # If there are filesystems that are not valid.
{
$text .= gettext ('These filesystems are not valid:')."\n";
foreach my $drive (sort @{$invalid})
{
$text .= "$drive ".$devices->desc($drive).' '.gettext ('Reason').': '.$devices->reason($drive)."\n";
}
$text .= "\n";
}
my $title = gettext('No valid backup device found');
$text .= "\n$title, ".gettext('please try again');
($rc, $choice) = $console->yesno_page
(
title => $title,
text => $text,
left => gettext('Try again'),
right => gettext('Cancel'),
);
if ($rc == 0) # Try Again
{
goto INITIATE_RESTORE;
}
else
{
$devices->destroy;
return;
}
}
# %found contains $backupcount backups.
if ($backupcount == 1)
{
# One backup found, so simple yes/no choice
$backupdrive = $found{1}{device}; # Find the (only) device that a backup was found on
$backupfile = $found{1}{path}; # find the actual backup
$time = gettext('Date') .' '. $found{1}{time};
$size = gettext('Size') .' '. $found{1}{sizeH};
($rc, $choice) = $console->yesno_page
(
title => gettext('Start restore from backup'),
text =>
gettext('Backup found on device').
"\n$backupdrive ".$devices->desc($backupdrive)."\n\n".
gettext('Backup details').
"\n$backupfile $size $time\n\n\n".
gettext('Do you wish to restore from this file?'),
);
goto INITIATE_RESTORE unless ($rc == 0);
$size = $found{1}{size};
}
else # Multiple backups found so display a choice
{
$text = gettext ('Backups found on these devices')."\n";
foreach my $backupfound (sort keys %found)
{
$backupdrive = $found{$backupfound}{device};
if (($backupfound == 1) || ($found{$backupfound}{device} ne $found{$backupfound-1}{device}))
{
$text.= "$backupdrive ".$devices->desc($backupdrive)."\n";
}
}
my @args = map { $_ => "$found{$_}{device} $found{$_}{path} $found{$_}{sizeH} $found{$_}{time}" } sort keys %found;
($rc, $choice) = $console->menu_page
(
title => gettext('Start restore from backup'),
text =>
"$text\n".
gettext ('Please select the backup that you wish to restore from.'),
argsref => \@args,
left => gettext('Cancel'),
right => gettext('OK'),
);
goto INITIATE_RESTORE unless ($rc == 0);
untaint $choice;
$backupdrive = $found{$choice}{device};
$size = $found{$choice}{size};
}
$devices->mount ($backupdrive); # mount the chosen filesystem
sleep(1); # Some mounts take time to become active
# Prepare for restore
$console->infobox(
title => gettext("Preparing for restore"),
text => gettext("Please stand by while the system is prepared for restore..."),
);
system("/sbin/e-smith/signal-event", "pre-restore");
# Create restore ID to check success later
my $restoreid = new_restore_id();
# Run restore
$console->gauge(make_restore_callback($mountpoint, $restoreid), 'title' => gettext('Restoring from backup'));
# Restore complete, now clean-up. Only post-upgrade and reboot if the restore was successful.
$devices->destroy;
if (-e "/tmp/restore_$restoreid") {
system("/sbin/e-smith/signal-event", "post-upgrade");
$db->set_prop("bootstrap-console", "Run", "yes");
$db->set_prop("bootstrap-console", "ForceSave", "yes");
$db->set_prop("bootstrap-console", "Restore", "disabled");
unless ( $self->{bootstrap} )
{
($rc, $choice) = $console->yesno_page
(
title => gettext("Restore Complete"),
text => gettext('You must restart your system to finish the restore.')."\n\n".
gettext('Do you want to restart now?'),
);
return unless ($rc == 0);
system("/usr/bin/tput", "clear");
system("/sbin/e-smith/signal-event", "reboot");
# A bit of a hack to avoid the console restarting before the
# reboot takes effect.
sleep(600);
}
}
return;
}
# Determine if this server is a fresh install for restore from backup
# Earlier the expression used was ($db->get_value('PasswordSet') eq 'yes')
# To prevent a restore return a negative number
# To allow a restore choose an appropiate sort order, eg 90
sub installOrder
{
my $order = (`grep :x:5...: /etc/group`) ? -1 : 90;
return $order;
}
#use esmith::console;
#esmith::console::perform_restore->new->doit(esmith::console->new,
# esmith::ConfigDB->open);
1;