# -- # Copyright (C) 2001-2019 OTRS AG, https://otrs.com/ # -- # This software comes with ABSOLUTELY NO WARRANTY. For details, see # the enclosed file COPYING for license information (GPL). If you # did not receive this file, see https://www.gnu.org/licenses/gpl-3.0.txt. # -- package Kernel::System::SupportBundleGenerator; use strict; use warnings; use Archive::Tar; our @ObjectDependencies = ( 'Kernel::Config', 'Kernel::System::CSV', 'Kernel::System::JSON', 'Kernel::System::Log', 'Kernel::System::Main', 'Kernel::System::Package', 'Kernel::System::Registration', 'Kernel::System::SupportDataCollector', 'Kernel::System::SysConfig', 'Kernel::System::DateTime', ); =head1 NAME Kernel::System::SupportBundleGenerator - support bundle generator =head1 DESCRIPTION All support bundle generator functions. =head1 PUBLIC INTERFACE =head2 new() Don't use the constructor directly, use the ObjectManager instead: my $SupportBundleGeneratorObject = $Kernel::OM->Get('Kernel::System::SupportBundleGenerator'); =cut sub new { my ( $Type, %Param ) = @_; # allocate new hash ref to object my $Self = {}; bless( $Self, $Type ); # cleanup the Home variable (remove tailing "/") $Self->{Home} = $Kernel::OM->Get('Kernel::Config')->Get('Home'); $Self->{Home} =~ s{\/\z}{}; $Self->{RandomID} = $Kernel::OM->Get('Kernel::System::Main')->GenerateRandomString( Length => 8, Dictionary => [ 0 .. 9, 'a' .. 'f' ], ); return $Self; } =head2 Generate() Generates a support bundle C<.tar> or C<.tar.gz> with the following contents: Registration Information, Support Data, Installed Packages, and another C<.tar> or C<.tar.gz> with all changed or new files in the OTRS installation directory. my $Result = $SupportBundleGeneratorObject->Generate(); Returns: $Result = { Success => 1, # Or false, in case of an error Data => { Filecontent => \$Tar, # Outer tar content reference Filename => 'SupportBundle.tar', # The outer tar filename Filesize => 123 # The size of the file in mega bytes }, =cut sub Generate { my ( $Self, %Param ) = @_; if ( !-e $Self->{Home} . '/ARCHIVE' ) { my $Message = $Self->{Home} . '/ARCHIVE: Is missing, can not continue!'; $Kernel::OM->Get('Kernel::System::Log')->Log( Priority => 'error', Message => $Message, ); return { Success => 0, Message => $Message, }; } my %SupportFiles; # get the list of installed packages ( $SupportFiles{PackageListContent}, $SupportFiles{PackageListFilename} ) = $Self->GeneratePackageList(); if ( !$SupportFiles{PackageListFilename} ) { my $Message = 'Can not generate the list of installed packages!'; $Kernel::OM->Get('Kernel::System::Log')->Log( Priority => 'error', Message => $Message, ); return { Success => 0, Message => $Message, }; } # get the registration information ( $SupportFiles{RegistrationInfoContent}, $SupportFiles{RegistrationInfoFilename} ) = $Self->GenerateRegistrationInfo(); if ( !$SupportFiles{RegistrationInfoFilename} ) { my $Message = 'Can not get the registration information!'; $Kernel::OM->Get('Kernel::System::Log')->Log( Priority => 'error', Message => $Message, ); return { Success => 0, Message => $Message, }; } # get the support data ( $SupportFiles{SupportDataContent}, $SupportFiles{SupportDataFilename} ) = $Self->GenerateSupportData(); if ( !$SupportFiles{SupportDataFilename} ) { my $Message = 'Can not collect the support data!'; $Kernel::OM->Get('Kernel::System::Log')->Log( Priority => 'error', Message => $Message, ); return { Success => 0, Message => $Message, }; } # get the archive of custom files ( $SupportFiles{CustomFilesArchiveContent}, $SupportFiles{CustomFilesArchiveFilename} ) = $Self->GenerateCustomFilesArchive(); if ( !$SupportFiles{CustomFilesArchiveFilename} ) { my $Message = 'Can not generate the custom files archive!'; $Kernel::OM->Get('Kernel::System::Log')->Log( Priority => 'error', Message => $Message, ); return { Success => 0, Message => $Message, }; } # get the configuration dump ( $SupportFiles{ConfigurationDumpContent}, $SupportFiles{ConfigurationDumpFilename} ) = $Self->GenerateConfigurationDump(); if ( !$SupportFiles{ConfigurationDumpFilename} ) { my $Message = 'Can not get the configuration dump!'; $Kernel::OM->Get('Kernel::System::Log')->Log( Priority => 'error', Message => $Message, ); return { Success => 0, Message => $Message, }; } # get config object my $ConfigObject = $Kernel::OM->Get('Kernel::Config'); # save and create archive my $TempDir = $ConfigObject->Get('TempDir') . '/SupportBundle'; if ( !-d $TempDir ) { mkdir $TempDir; } $TempDir = $ConfigObject->Get('TempDir') . '/SupportBundle/' . $Self->{RandomID}; if ( !-d $TempDir ) { mkdir $TempDir; } # remove all files my @ListOld = glob( $TempDir . '/*' ); for my $File (@ListOld) { unlink $File; } # get main object my $MainObject = $Kernel::OM->Get('Kernel::System::Main'); my @List; for my $Key (qw(PackageList RegistrationInfo SupportData CustomFilesArchive ConfigurationDump)) { if ( $SupportFiles{ $Key . 'Filename' } && $SupportFiles{ $Key . 'Content' } ) { my $Location = $TempDir . '/' . $SupportFiles{ $Key . 'Filename' }; my $Content = $SupportFiles{ $Key . 'Content' }; my $FileLocation = $MainObject->FileWrite( Location => $Location, Content => $Content, Mode => 'binmode', Type => 'Local', Permission => '644', ); push @List, $Location; } } my $DateTimeObject = $Kernel::OM->Create('Kernel::System::DateTime'); my $Filename = "SupportBundle_" . $DateTimeObject->Format( Format => "%Y-%m-%d_%H-%M" ); # add files to the tar archive my $Archive = $TempDir . '/' . $Filename; my $TarObject = Archive::Tar->new(); $TarObject->add_files(@List); $TarObject->write( $Archive, 0 ) || die "Could not write: $_!"; # add files to the tar archive open( my $Tar, '<', $Archive ); ## no critic binmode $Tar; my $TmpTar = do { local $/; <$Tar> }; close $Tar; # remove all files @ListOld = glob( $TempDir . '/*' ); for my $File (@ListOld) { unlink $File; } # remove temporary directory rmdir $TempDir; if ( $Kernel::OM->Get('Kernel::System::Main')->Require('Compress::Zlib') ) { my $GzTar = Compress::Zlib::memGzip($TmpTar); # log info $Kernel::OM->Get('Kernel::System::Log')->Log( Priority => 'notice', Message => 'Download Compress::Zlib end', ); return { Success => 1, Data => { Filecontent => \$GzTar, Filename => $Filename . '.tar.gz', Filesize => bytes::length($GzTar) / ( 1024 * 1024 ), }, }; } # log info $Kernel::OM->Get('Kernel::System::Log')->Log( Priority => 'notice', Message => 'Download no Compress::Zlib end', ); return { Success => 1, Data => { Filecontent => \$TmpTar, Filename => $Filename . '.tar', Filesize => bytes::length($TmpTar) / ( 1024 * 1024 ), }, }; } =head2 GenerateCustomFilesArchive() Generates a C<.tar> or C<.tar.gz> file with all eligible changed or added files taking the ARCHIVE file as a reference my ( $Content, $Filename ) = $SupportBundleGeneratorObject->GenerateCustomFilesArchive(); Returns: $Content = $FileContentsRef; $Filename = 'application.tar'; # or 'application.tar.gz' =cut sub GenerateCustomFilesArchive { my ( $Self, %Param ) = @_; # get config object my $ConfigObject = $Kernel::OM->Get('Kernel::Config'); my $TempDir = $ConfigObject->Get('TempDir') . '/SupportBundle'; if ( !-d $TempDir ) { mkdir $TempDir; } $TempDir = $ConfigObject->Get('TempDir') . '/SupportBundle/' . $Self->{RandomID}; if ( !-d $TempDir ) { mkdir $TempDir; } # remove all files my @ListOld = glob( $TempDir . '/*' ); for my $File (@ListOld) { unlink $File; } my $CustomFilesArchive = $TempDir . '/application.tar'; if ( -f $CustomFilesArchive ) { unlink $CustomFilesArchive || die "Can't unlink $CustomFilesArchive: $!"; } # get a MD5Sum lookup table from all known files (from framework and packages) $Self->{MD5SumLookup} = $Self->_GetMD5SumLookup(); # get the list of file to add to the Dump my @List = $Self->_GetCustomFileList( Directory => $Self->{Home} ); # add files to the Dump my $TarObject = Archive::Tar->new(); $TarObject->add_files(@List); # within the tar file the paths are not absolute, so leading "/" must be removed my $HomeWithoutSlash = $Self->{Home}; $HomeWithoutSlash =~ s{\A\/}{}; # Mask passwords in Config files. CONFIGFILE: for my $ConfigFile ( $TarObject->list_files() ) { next CONFIGFILE if ( $ConfigFile !~ 'Kernel/Config.pm' && $ConfigFile !~ 'Kernel/Config/Files' ); my $Content = $TarObject->get_content($ConfigFile); if ( !$Content ) { $Kernel::OM->Get('Kernel::System::Log')->Log( Priority => 'error', Message => "$ConfigFile was not found in the modified files!", ); next CONFIGFILE; } $Content = $Self->_MaskPasswords( StringToMask => $Content, ); $TarObject->replace_content( $ConfigFile, $Content ); } my $Write = $TarObject->write( $CustomFilesArchive, 0 ); if ( !$Write ) { # log info $Kernel::OM->Get('Kernel::System::Log')->Log( Priority => 'error', Message => "Can't write $CustomFilesArchive: $!", ); return; } # add files to the tar archive my $TARFH; if ( !open( $TARFH, '<', $CustomFilesArchive ) ) { ## no critic # log info $Kernel::OM->Get('Kernel::System::Log')->Log( Priority => 'error', Message => "Can't read $CustomFilesArchive: $!", ); return; } binmode $TARFH; my $TmpTar = do { local $/; <$TARFH> }; close $TARFH; if ( $Kernel::OM->Get('Kernel::System::Main')->Require('Compress::Zlib') ) { my $GzTar = Compress::Zlib::memGzip($TmpTar); # log info $Kernel::OM->Get('Kernel::System::Log')->Log( Priority => 'notice', Message => "Compression of $CustomFilesArchive end", ); return ( \$GzTar, 'application.tar.gz' ); } # log info $Kernel::OM->Get('Kernel::System::Log')->Log( Priority => 'notice', Message => "$CustomFilesArchive was not compressed", ); return ( \$TmpTar, 'application.tar' ); } =head2 GeneratePackageList() Generates a .csv file with all installed packages my ( $Content, $Filename ) = $SupportBundleGeneratorObject->GeneratePackageList(); Returns: $Content = $FileContentsRef; $Filename = 'InstalledPackages.csv'; =cut sub GeneratePackageList { my ( $Self, %Param ) = @_; my @PackageList = $Kernel::OM->Get('Kernel::System::Package')->RepositoryList( Result => 'Short' ); # get csv object my $CSVObject = $Kernel::OM->Get('Kernel::System::CSV'); my $CSVContent = ''; for my $Package (@PackageList) { my @PackageData = ( [ $Package->{Name}, $Package->{Version}, $Package->{MD5sum}, $Package->{Vendor}, ], ); # convert data into CSV string $CSVContent .= $CSVObject->Array2CSV( Data => \@PackageData, ); } return ( \$CSVContent, 'InstalledPackages.csv' ); } =head2 GenerateRegistrationInfo() Generates a C<.json> file with the otrs system registration information my ( $Content, $Filename ) = $SupportBundleGeneratorObject->GenerateRegistrationInfo(); Returns: $Content = $FileContentsRef; $Filename = 'RegistrationInfo.json'; =cut sub GenerateRegistrationInfo { my ( $Self, %Param ) = @_; my %RegistrationInfo = $Kernel::OM->Get('Kernel::System::Registration')->RegistrationDataGet( Extended => 1, ); my %Data; if (%RegistrationInfo) { my $State = $RegistrationInfo{State} || ''; if ( $State && lc $State eq 'registered' ) { $State = 'active'; } %Data = ( %{ $RegistrationInfo{System} }, State => $State, APIVersion => $RegistrationInfo{APIVersion}, APIKey => $RegistrationInfo{APIKey}, LastUpdateID => $RegistrationInfo{LastUpdateID}, RegistrationKey => $RegistrationInfo{UniqueID}, SupportDataSending => $RegistrationInfo{SupportDataSending}, Type => $RegistrationInfo{Type}, Description => $RegistrationInfo{Description}, ); } else { %Data = %RegistrationInfo; } my $JSONContent = $Kernel::OM->Get('Kernel::System::JSON')->Encode( Data => \%Data, ); return ( \$JSONContent, 'RegistrationInfo.json' ); } =head2 GenerateConfigurationDump() Generates a <.yml> file with the otrs system registration information my ( $Content, $Filename ) = $SupportBundleGeneratorObject->GenerateConfigurationDump(); Returns: $Content = $FileContentsRef; $Filename = <'ModifiedSettings.yml'>; =cut sub GenerateConfigurationDump { my ( $Self, %Param ) = @_; my $Export = $Kernel::OM->Get('Kernel::System::SysConfig')->ConfigurationDump( SkipDefaultSettings => 1, ); $Export = $Self->_MaskPasswords( StringToMask => $Export, YAML => 1 ); return ( \$Export, 'ModifiedSettings.yml' ); } =head2 GenerateSupportData() Generates a C<.json> file with the support data my ( $Content, $Filename ) = $SupportBundleGeneratorObject->GenerateSupportData(); Returns: $Content = $FileContentsRef; $Filename = 'GenerateSupportData.json'; =cut sub GenerateSupportData { my ( $Self, %Param ) = @_; my $SupportDataCollectorWebTimeout = $Kernel::OM->Get('Kernel::Config')->Get('SupportDataCollector::WebUserAgent::Timeout'); my %SupportData = $Kernel::OM->Get('Kernel::System::SupportDataCollector')->Collect( WebTimeout => $SupportDataCollectorWebTimeout, ); my $JSONContent = $Kernel::OM->Get('Kernel::System::JSON')->Encode( Data => \%SupportData, ); return ( \$JSONContent, 'SupportData.json' ); } sub _GetMD5SumLookup { my ( $Self, %Param ) = @_; # generate a MD5 Sum lookup table from framework ARCHIVE my $FileList = $Kernel::OM->Get('Kernel::System::Main')->FileRead( Location => $Self->{Home} . '/ARCHIVE', Mode => 'utf8', Type => 'Local', Result => 'ARRAY', DisableWarnings => 1, ); my %MD5SumLookup; for my $Line ( @{$FileList} ) { my ( $MD5Sum, $File ) = split /::/, $Line; chomp $File; $MD5SumLookup{ $Self->{Home} . '/' . $File } = $MD5Sum; } # get package object my $PackageObject = $Kernel::OM->Get('Kernel::System::Package'); # get a list of packages installed my @PackagesList = $PackageObject->RepositoryList( Result => 'short', ); # get from each installed package a MD5 Sum Lookup table and store it on a global Lookup table my %PackageMD5SumLookup; for my $Package (@PackagesList) { my $PartialMD5Sum = $PackageObject->PackageFileGetMD5Sum( %{$Package} ); %PackageMD5SumLookup = ( %PackageMD5SumLookup, %{$PartialMD5Sum} ); } # add MD5Sums from all packages to the list from framework ARCHIVE # overwritten files by packages will also overwrite the MD5 Sum %MD5SumLookup = ( %MD5SumLookup, %PackageMD5SumLookup ); return \%MD5SumLookup; } sub _GetCustomFileList { my ( $Self, %Param ) = @_; # check needed stuff for my $Needed (qw(Directory)) { if ( !$Param{$Needed} ) { $Kernel::OM->Get('Kernel::System::Log')->Log( Priority => 'error', Message => "Need $Needed!" ); return; } } # get config object my $ConfigObject = $Kernel::OM->Get('Kernel::Config'); # article directory my $ArticleDir = $ConfigObject->Get('Ticket::Article::Backend::MIMEBase::ArticleDataDir'); # cleanup file name $ArticleDir =~ s/\/\//\//g; # temp directory my $TempDir = $ConfigObject->Get('TempDir'); # cleanup file name $TempDir =~ s/\/\//\//g; # check all $Param{Directory}/* in home directory my @Files; my @List = glob("$Param{Directory}/*"); FILE: for my $File (@List) { # cleanup file name $File =~ s/\/\//\//g; # check if directory if ( -d $File ) { # do not include article in file system next FILE if $File =~ /\Q$ArticleDir\E/i; # do not include tmp in file system next FILE if $File =~ /\Q$TempDir\E/i; # do not include js-cache next FILE if $File =~ /js-cache/; # do not include css-cache next FILE if $File =~ /css-cache/; # do not include documentation next FILE if $File =~ /doc/; # add directory to list push @Files, $Self->_GetCustomFileList( Directory => $File ); } else { # do not include hidden files next FILE if $File =~ /^\./; # do not include files with # in file name next FILE if $File =~ /#/; # do not include previous system dumps next FILE if $File =~ /.tar/; # do not include ARCHIVE next FILE if $File =~ /ARCHIVE/; # do not include if file is not readable next FILE if !-r $File; my $MD5Sum = $Kernel::OM->Get('Kernel::System::Main')->MD5sum( Filename => $File, ); # check if is a known file, in such case, check if MD5 is the same as the expected # skip file if MD5 matches if ( $Self->{MD5SumLookup}->{$File} && $Self->{MD5SumLookup}->{$File} eq $MD5Sum ) { next FILE; } # add file to list push @Files, $File; } } return @Files; } sub _MaskPasswords { my ( $Self, %Param ) = @_; # check needed stuff for my $Needed (qw(StringToMask)) { if ( !$Param{$Needed} ) { $Kernel::OM->Get('Kernel::System::Log')->Log( Priority => 'error', Message => "Need $Needed!" ); return; } } my $StringToMask = $Param{StringToMask}; # Trim any passswords. # Simple settings like $Self->{'DatabasePw'} or $Self->{'AuthModule::LDAP::SearchUserPw1'}. $StringToMask =~ s/(\$Self->\{'*[^']+(?:Password|Pw)\d*'*\}\s*=\s*)\'.*?\'/$1\'xxx\'/mg; # Complex settings like: # $Self->{CustomerUser1} = { # Params => { # UserPw => 'xxx', $StringToMask =~ s/((?:Password|Pw)\d*\s*=>\s*)\'.*?\'/$1\'xxx\'/mg; return $StringToMask; } =head1 TERMS AND CONDITIONS This software is part of the OTRS project (L). This software comes with ABSOLUTELY NO WARRANTY. For details, see the enclosed file COPYING for license information (GPL). If you did not receive this file, see L. =cut 1;