1#!/usr/local/bin/perl 2# -- 3# Copyright (C) 2001-2020 OTRS AG, https://otrs.com/ 4# -- 5# This program is free software: you can redistribute it and/or modify 6# it under the terms of the GNU General Public License as published by 7# the Free Software Foundation, either version 3 of the License, or 8# (at your option) any later version. 9# 10# This program is distributed in the hope that it will be useful, 11# but WITHOUT ANY WARRANTY; without even the implied warranty of 12# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13# GNU General Public License for more details. 14# 15# You should have received a copy of the GNU General Public License 16# along with this program. If not, see https://www.gnu.org/licenses/gpl-3.0.txt. 17# -- 18 19use strict; 20use warnings; 21 22# use ../ as lib location 23use File::Basename; 24use FindBin qw($RealBin); 25use lib dirname($RealBin); 26use lib dirname($RealBin) . "/Kernel/cpan-lib"; 27 28use Getopt::Std; 29 30use Kernel::System::ObjectManager; 31 32# get options 33my %Opts; 34my $Compress = ''; 35my $CompressCMD = ''; 36my $CompressEXT = ''; 37my $DB = ''; 38my $DBDump = ''; 39getopt( 'hcrtd', \%Opts ); 40 41if ( exists $Opts{h} ) { 42 print <<EOF; 43 44Backup an OTRS system. 45 46Usage: 47 backup.pl -d /data_backup_dir [-c gzip|bzip2] [-r DAYS] [-t fullbackup|nofullbackup|dbonly] 48 49Options: 50 -d - Directory where the backup files should place to. 51 [-c] - Select the compression method (gzip|bzip2). Default: gzip. 52 [-r DAYS] - Remove backups which are more than DAYS days old. 53 [-t] - Specify which data will be saved (fullbackup|nofullbackup|dbonly). Default: fullbackup. 54 [-h] - Display help for this command. 55 56Help: 57Using -t fullbackup saves the database and the whole OTRS home directory (except /var/tmp and cache directories). 58Using -t nofullbackup saves only the database, /Kernel/Config* and /var directories. 59With -t dbonly only the database will be saved. 60 61Output: 62 Config.tar.gz - Backup of /Kernel/Config* configuration files. 63 Application.tar.gz - Backup of application file system (in case of full backup). 64 VarDir.tar.gz - Backup of /var directory (in case of no full backup). 65 DataDir.tar.gz - Backup of article files. 66 DatabaseBackup.sql.gz - Database dump. 67 68EOF 69 exit 1; 70} 71 72# check backup dir 73if ( !$Opts{d} ) { 74 print STDERR "ERROR: Need -d for backup directory\n"; 75 exit 1; 76} 77elsif ( !-d $Opts{d} ) { 78 print STDERR "ERROR: No such directory: $Opts{d}\n"; 79 exit 1; 80} 81 82# check compress mode 83if ( $Opts{c} && $Opts{c} =~ m/bzip2/i ) { 84 $Compress = 'j'; 85 $CompressCMD = 'bzip2'; 86 $CompressEXT = 'bz2'; 87} 88else { 89 $Compress = 'z'; 90 $CompressCMD = 'gzip'; 91 $CompressEXT = 'gz'; 92} 93 94# check backup type 95my $DBOnlyBackup = 0; 96my $FullBackup = 0; 97 98if ( $Opts{t} && $Opts{t} eq 'dbonly' ) { 99 $DBOnlyBackup = 1; 100} 101elsif ( $Opts{t} && $Opts{t} eq 'nofullbackup' ) { 102 $FullBackup = 0; 103} 104else { 105 $FullBackup = 1; 106} 107 108# create common objects 109local $Kernel::OM = Kernel::System::ObjectManager->new( 110 'Kernel::System::Log' => { 111 LogPrefix => 'OTRS-backup.pl', 112 }, 113); 114 115my $DatabaseHost = $Kernel::OM->Get('Kernel::Config')->Get('DatabaseHost'); 116my $Database = $Kernel::OM->Get('Kernel::Config')->Get('Database'); 117my $DatabaseUser = $Kernel::OM->Get('Kernel::Config')->Get('DatabaseUser'); 118my $DatabasePw = $Kernel::OM->Get('Kernel::Config')->Get('DatabasePw'); 119my $DatabaseDSN = $Kernel::OM->Get('Kernel::Config')->Get('DatabaseDSN'); 120my $ArticleDir = $Kernel::OM->Get('Kernel::Config')->Get('Ticket::Article::Backend::MIMEBase::ArticleDataDir'); 121 122# decrypt pw (if needed) 123if ( $DatabasePw =~ m/^\{(.*)\}$/ ) { 124 $DatabasePw = $Kernel::OM->Get('Kernel::System::DB')->_Decrypt($1); 125} 126 127# check db backup support 128if ( $DatabaseDSN =~ m/:mysql/i ) { 129 $DB = 'MySQL'; 130 $DBDump = 'mysqldump'; 131} 132elsif ( $DatabaseDSN =~ m/:pg/i ) { 133 $DB = 'PostgreSQL'; 134 $DBDump = 'pg_dump'; 135 if ( $DatabaseDSN !~ m/host=/i ) { 136 $DatabaseHost = ''; 137 } 138} 139else { 140 print STDERR "ERROR: Can't backup, no database dump support!\n"; 141 exit(1); 142} 143 144# check needed programs 145for my $CMD ( 'cp', 'tar', $DBDump, $CompressCMD ) { 146 my $Installed = 0; 147 open my $In, '-|', "which $CMD"; ## no critic 148 while (<$In>) { 149 $Installed = 1; 150 } 151 close $In; 152 if ( !$Installed ) { 153 print STDERR "ERROR: Can't locate $CMD!\n"; 154 exit 1; 155 } 156} 157 158# create new backup directory 159my $Home = $Kernel::OM->Get('Kernel::Config')->Get('Home'); 160 161# append trailing slash to home directory, if it's missing 162if ( $Home !~ m{\/\z} ) { 163 $Home .= '/'; 164} 165 166chdir($Home); 167 168# create directory name - this looks like 2013-09-09_22-19' 169my $SystemDTObject = $Kernel::OM->Create('Kernel::System::DateTime'); 170my $Directory = $SystemDTObject->Format( 171 Format => $Opts{d} . '/%Y-%m-%d_%H-%M', 172); 173 174if ( !mkdir($Directory) ) { 175 die "ERROR: Can't create directory: $Directory: $!\n"; 176} 177 178# backup application 179if ($DBOnlyBackup) { 180 print "Backup of filesystem data disabled by parameter dbonly ... \n"; 181} 182else { 183 # backup Kernel/Config.pm 184 print "Backup $Directory/Config.tar.$CompressEXT ... "; 185 if ( !system("tar -c -$Compress -f $Directory/Config.tar.$CompressEXT Kernel/Config*") ) { 186 print "done\n"; 187 } 188 else { 189 print "failed\n"; 190 RemoveIncompleteBackup($Directory); 191 die "Backup failed\n"; 192 } 193 194 if ($FullBackup) { 195 print "Backup $Directory/Application.tar.$CompressEXT ... "; 196 my $Excludes = "--exclude=var/tmp --exclude=js-cache --exclude=css-cache --exclude=.git"; 197 if ( !system("tar $Excludes -c -$Compress -f $Directory/Application.tar.$CompressEXT .") ) { 198 print "done\n"; 199 } 200 else { 201 print "failed\n"; 202 RemoveIncompleteBackup($Directory); 203 die "Backup failed\n"; 204 } 205 } 206 207 # backup vardir 208 else { 209 print "Backup $Directory/VarDir.tar.$CompressEXT ... "; 210 if ( !system("tar -c -$Compress -f $Directory/VarDir.tar.$CompressEXT var/") ) { 211 print "done\n"; 212 } 213 else { 214 print "failed\n"; 215 RemoveIncompleteBackup($Directory); 216 die "Backup failed\n"; 217 } 218 } 219 220 # backup datadir 221 if ( $ArticleDir !~ m/\A\Q$Home\E/ ) { 222 print "Backup $Directory/DataDir.tar.$CompressEXT ... "; 223 if ( !system("tar -c -$Compress -f $Directory/DataDir.tar.$CompressEXT $ArticleDir") ) { 224 print "done\n"; 225 } 226 else { 227 print "failed\n"; 228 RemoveIncompleteBackup($Directory); 229 die "Backup failed\n"; 230 } 231 } 232} 233 234# backup database 235my $ErrorIndicationFileName = 236 $Kernel::OM->Get('Kernel::Config')->Get('Home') 237 . '/var/tmp/' 238 . $Kernel::OM->Get('Kernel::System::Main')->GenerateRandomString(); 239if ( $DB =~ m/mysql/i ) { 240 print "Dump $DB data to $Directory/DatabaseBackup.sql.$CompressEXT ... "; 241 if ($DatabasePw) { 242 $DatabasePw = "-p'$DatabasePw'"; 243 } 244 if ( 245 !system( 246 "( $DBDump -u $DatabaseUser $DatabasePw -h $DatabaseHost $Database || touch $ErrorIndicationFileName ) | $CompressCMD > $Directory/DatabaseBackup.sql.$CompressEXT" 247 ) 248 && !-f $ErrorIndicationFileName 249 ) 250 { 251 print "done\n"; 252 } 253 else { 254 print "failed\n"; 255 if ( -f $ErrorIndicationFileName ) { 256 unlink $ErrorIndicationFileName; 257 } 258 RemoveIncompleteBackup($Directory); 259 die "Backup failed\n"; 260 } 261} 262else { 263 print "Dump $DB data to $Directory/DatabaseBackup.sql ... "; 264 265 # set password via environment variable if there is one 266 if ($DatabasePw) { 267 $ENV{'PGPASSWORD'} = $DatabasePw; ## no critic 268 } 269 270 if ($DatabaseHost) { 271 $DatabaseHost = "-h $DatabaseHost"; 272 } 273 274 if ( 275 !system( 276 "( $DBDump $DatabaseHost -U $DatabaseUser $Database || touch $ErrorIndicationFileName ) | $CompressCMD > $Directory/DatabaseBackup.sql.$CompressEXT" 277 ) 278 && !-f $ErrorIndicationFileName 279 ) 280 { 281 print "done\n"; 282 } 283 else { 284 print "failed\n"; 285 if ( -f $ErrorIndicationFileName ) { 286 unlink $ErrorIndicationFileName; 287 } 288 RemoveIncompleteBackup($Directory); 289 die "Backup failed\n"; 290 } 291} 292 293# remove old backups only after everything worked well 294if ( defined $Opts{r} ) { 295 my %LeaveBackups; 296 297 # we'll be substracting days to the current time 298 # we don't want DST changes to affect our dates 299 # if it is < 2:00 AM, add two hours so we're sure DST will not change our timestamp 300 # to another day 301 if ( $SystemDTObject->Get()->{Hour} < 2 ) { 302 $SystemDTObject->Add( Hours => 2 ); 303 } 304 305 for ( 0 .. $Opts{r} ) { 306 307 # legacy, old directories could be in the format 2013-4-8 308 my @LegacyDirFormats = ( 309 '%04d-%01d-%01d', 310 '%04d-%02d-%01d', 311 '%04d-%01d-%02d', 312 '%04d-%02d-%02d', 313 ); 314 315 my $SystemDTDetails = $SystemDTObject->Get(); 316 for my $LegacyFirFormat (@LegacyDirFormats) { 317 my $Dir = sprintf( 318 $LegacyFirFormat, 319 $SystemDTDetails->{Year}, 320 $SystemDTDetails->{Month}, 321 $SystemDTDetails->{Day}, 322 ); 323 $LeaveBackups{$Dir} = 1; 324 } 325 326 # substract one day 327 $SystemDTObject->Subtract( Days => 1 ); 328 } 329 330 my @Directories = $Kernel::OM->Get('Kernel::System::Main')->DirectoryRead( 331 Directory => $Opts{d}, 332 Filter => '*', 333 ); 334 335 DIRECTORY: 336 for my $Directory (@Directories) { 337 next DIRECTORY if !-d $Directory; 338 my $Leave = 0; 339 for my $Data ( sort keys %LeaveBackups ) { 340 if ( $Directory =~ m/$Data/ ) { 341 $Leave = 1; 342 } 343 } 344 if ( !$Leave ) { 345 346 # remove files and directory 347 print "deleting old backup in $Directory ... "; 348 my @Files = $Kernel::OM->Get('Kernel::System::Main')->DirectoryRead( 349 Directory => $Directory, 350 Filter => '*', 351 ); 352 for my $File (@Files) { 353 if ( -e $File ) { 354 355 # print "Notice: remove $File\n"; 356 unlink $File; 357 } 358 } 359 if ( rmdir($Directory) ) { 360 print "done\n"; 361 } 362 else { 363 die "failed\n"; 364 } 365 } 366 } 367} 368 369# If error occurs this functions remove incomlete backup folder to avoid the impression 370# that the backup was ok (see http://bugs.otrs.org/show_bug.cgi?id=10665). 371sub RemoveIncompleteBackup { 372 373 # get parameters 374 my $Directory = shift; 375 376 # remove files and directory 377 print STDERR "Deleting incomplete backup $Directory ... "; 378 my @Files = $Kernel::OM->Get('Kernel::System::Main')->DirectoryRead( 379 Directory => $Directory, 380 Filter => '*', 381 ); 382 for my $File (@Files) { 383 if ( -e $File ) { 384 unlink $File; 385 } 386 } 387 if ( rmdir($Directory) ) { 388 print STDERR "done\n"; 389 } 390 else { 391 print STDERR "failed\n"; 392 } 393 394 return; 395} 396