Files
scripts/Perl OTRS/Kernel/System/SupportBundleGenerator.pm
2024-10-14 00:08:40 +02:00

731 lines
20 KiB
Perl

# --
# 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<https://otrs.org/>).
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<https://www.gnu.org/licenses/gpl-3.0.txt>.
=cut
1;