Files
server/opt/psa/bin/transvhosts.pl
2026-01-07 20:52:11 +01:00

556 lines
17 KiB
Perl
Executable File

#!/usr/bin/perl
# Copyright 1999-2025. WebPros International GmbH. All rights reserved.
#########################################################################################################################
# #
# transvhost.pl - Utility change HTTPD_VHOSTS_D psa.conf value: moves content and corrects database and config files. #
# #
#########################################################################################################################
use strict;
use warnings;
use File::Find ();
use File::Temp qw(tempfile);
use IO::File;
use JSON::XS;
use vars qw($newVhostsPath $oldVhostsPath %config %domainHash %statHash %logRotHash);
$ENV{"LC_ALL"}="C";
my %arg_opts = ('--help|-h'=>'',
'--dest-dir|-d'=>'s',
'--correct-scripts'=>'',
);
my $ptrArgs = getArguments(\@ARGV,\%arg_opts);
if (exists $ptrArgs->{'help'}){
printHelp($0);
exit(0);
}
if (!exists $ptrArgs->{'dest-dir'}) {
printf ("You should specify destination directory.\n");
printHelp($0);
exit(2);
}
read_config();
$oldVhostsPath = $config{HTTPD_VHOSTS_D};
$newVhostsPath = $ptrArgs->{'dest-dir'};
if ($newVhostsPath =~ m/\/$/){
# remove slash at the end of path
($newVhostsPath) = $newVhostsPath =~ m/^(.*)\/$/;
}
my $getenforce = '/usr/sbin/getenforce';
if (-x $getenforce and `$getenforce` =~ /Enforcing/i) {
print STDERR "Before transferring virtual hosts' content, disable SELinux " .
"or set it to permissive mode and keep it that way to avoid breaking your websites.\n";
print STDERR "Read more: " .
"https://support.plesk.com/hc/en-us/articles/12377953257111-How-to-change-virtual-hosts-location-in-Plesk-for-Linux\n";
exit(1);
}
detectFileSystem();
correctPhpFpmPools();
fail2BanInstalled() and corrertFail2BanApacheJails();
correctDb();
reconfigureAps();
if(system($config{WEBSERV} . " --reconfigure-all > /dev/null") !=0) {
printf("Can`t reconfigure web server \n");
}
if(system($config{FTPSERV} . " --reconfigure-all > /dev/null") !=0) {
printf("Can`t reconfigure ftp server \n");
}
createDomainHash();
createLogRotHash();
correctPhpIniFiles();
foreach my $domain (keys %statHash) {
system($config{WEBSTAT}. " --unset-configs --stat-prog=$statHash{$domain} --domain-name=$domain");
system($config{WEBSTAT}. " --set-configs --stat-prog=$statHash{$domain} --domain-name=$domain < /dev/null");
}
foreach my $domain (keys %logRotHash) {
system($config{LOGROT}. " $domain off $logRotHash{$domain}");
system($config{LOGROT}. " $domain on $logRotHash{$domain}");
}
if (exists $ptrArgs->{'correct-scripts'}) {
if (correctScripts()!=0){
exit -1;
}
exit 0;
}
exit 0;
sub read_config {
open FCONF, "< /etc/psa/psa.conf"
or die "Can't open Plesk configurational file\n";
while (<FCONF>) {
s/\#.*$//;
m/^\s*(\w+)\s+(.+?)\s*$/;
next unless $1;
$config{$1} = $2;
}
close FCONF;
# prepare temporary file with mysql login and password
# that will be kept until the finish of the util execution
my ($myExtFH, $myExtFN) = tempfile(UNLINK => 1);
print $myExtFH "[client]\n";
print $myExtFH "user=admin\n";
print $myExtFH "password=";
close $myExtFH;
0 == system("cat /etc/psa/.psa.shadow >> \"$myExtFN\"")
or die "Can't get Plesk administrator's password\n";
if (-x $config{MYSQL_BIN_D}.'/mariadb') {
$config{MYSQL} = $config{MYSQL_BIN_D}."/mariadb --defaults-extra-file=\"$myExtFN\" -s -N -Dpsa";
} else {
$config{MYSQL} = $config{MYSQL_BIN_D}."/mysql --defaults-extra-file=\"$myExtFN\" -s -N -Dpsa";
}
if (-e '/etc/SuSE-release' or -e '/etc/debian_version') {
$config{MYSQL_SCRIPT} = $config{PRODUCT_RC_D} . '/mysql';
} else {
$config{MYSQL_SCRIPT} = $config{PRODUCT_RC_D} . '/mysqld';
}
$config{WEBSERV} = $config{PRODUCT_ROOT_D}.'/admin/bin/httpdmng';
$config{FTPSERV} = $config{PRODUCT_ROOT_D}.'/admin/bin/ftpmng';
$config{WEBSTAT} = $config{PRODUCT_ROOT_D}.'/admin/bin/webstatmng';
$config{LOGROT} = $config{PRODUCT_ROOT_D}.'/admin/bin/logrot_mng';
return 0;
}
sub find(&@) { &File::Find::find }
sub correctFileSystem{
if ($newVhostsPath eq $oldVhostsPath) {
print "Server is already configured.\n";
exit 0;
}
print "Moving files to new directory...\n";
my $mkdir = `mkdir -p $newVhostsPath` if (! -d '$newVhostsPath');
system("mv $oldVhostsPath/* $oldVhostsPath/\.* $newVhostsPath 2>/dev/null");
#correct psa.conf
my $psaSed = "-e \"s|$oldVhostsPath|$newVhostsPath|g\"";
print "Correct psa configuration file...\n";
system("sed $psaSed /etc/psa/psa.conf > /etc/psa/psa.conf.new");
system("cp /etc/psa/psa.conf.new /etc/psa/psa.conf");
if ($? == 0) {
unlink "/etc/psa/psa.conf.new";
}
#correct /etc/passwd
print "Correct passwd file...\n";
system("sed $psaSed /etc/passwd > /etc/passwd.new");
system("cp /etc/passwd.new /etc/passwd");
if ($? == 0) {
unlink "/etc/passwd.new";
}
setDirtyFlag();
return 0;
}
sub createLogRotHash {
my $query = "select d.name, l.turned_on, l.period_type, l.period, l.max_number_of_logfiles, l.compress_enable, l.email from domains d, hosting h, log_rotation l, dom_param dp where d.htype='vrt_hst' and d.id=dp.dom_id and dp.val=l.id";
my $state;
my $command = $config{MYSQL}." -e \"$query\"";
open (QUERY, "$command |");
while (<QUERY>){
if (m/([\S]+)\s([\S]+)\s([\S]+)\s([\S]+)\s([\S]+)\s([\S]+)\s(.*)/){
next if ($2 eq 'false');
$logRotHash{$1} = join(" ", $3, $4, $5, $6, $7);
}
}
close (QUERY);
}
sub createDomainHash {
my $query = "SELECT d.id, d.name, h.webstat FROM domains d LEFT JOIN hosting h ON d.id=h.dom_id WHERE d.htype='vrt_hst'";
my $command = $config{MYSQL}." -e \"$query\"";
open (QUERY, "$command |");
while (<QUERY>){
if (m/([\S]+)\s([\S]+)\s([\S]+)/){
$domainHash{$2} = $1;
next if $3 eq 'none';
$statHash{$2} = $3;
}
}
close (QUERY);
}
sub correctDb {
my ($query, $command);
print "Correct database...\n";
print "Update hosting settings...\n";
$query = "UPDATE hosting SET www_root = REPLACE(www_root, '$oldVhostsPath', '$newVhostsPath')";
$command = $config{MYSQL}." -e \"$query\"";
system($command);
print "done\n";
print "Update subdomains settings...\n";
$query = "UPDATE subdomains SET www_root = REPLACE(www_root, '$oldVhostsPath', '$newVhostsPath')";
$command = $config{MYSQL}." -e \"$query\"";
system($command);
print "done\n";
print "Update system users settings...\n";
$query = "UPDATE sys_users SET home = REPLACE(home, '$oldVhostsPath', '$newVhostsPath')";
$command = $config{MYSQL}." -e \"$query\"";
system($command);
print "done\n";
print "Update aps resources parameters...\n";
$query = "UPDATE apsResourcesParameters SET value = REPLACE(value, '$oldVhostsPath', '$newVhostsPath')";
$command = $config{MYSQL}." -e \"$query\"";
system($command);
print "done\n";
$command = $config{PRODUCT_ROOT_D}."/bin/sw-engine-pleskrun ".$config{PRODUCT_ROOT_D}."/admin/plib/api-cli/service_node.php --update local";
system($command);
}
sub reconfigureAps{
print "Reconfigure aps applications...\n";
my $command = $config{PRODUCT_ROOT_D}."/bin/sw-engine-pleskrun ".$config{PRODUCT_ROOT_D}."/admin/plib/api-cli/aps.php --reconfigure-all";
system($command);
print "done\n";
}
sub check_mysql() {
printf("Attempting to connect to MySQL: ");
my $res = system($config{MYSQL} . " -e '' 2> /dev/null");
printf("%s\n", ($res ? "failed" : "ok"));
return $res;
}
sub correctFile($) {
my $file = shift;
open(my $fh, "+<$file") or die "Cannot open file: $! : $file .\n";
my $out = '';
while (<$fh>) {
s/$oldVhostsPath(?![\w\.])/$newVhostsPath/g;
$out .= $_;
}
seek($fh, 0, 0);
print $fh $out;
truncate($fh, tell($fh));
close($fh);
}
sub correctScripts{
unless (-d $newVhostsPath){
print "Directory $newVhostsPath does not exist.\n";
return -1;
}
print "Correct user scripts...\n";
my @skip_dirs = qw/bin dev lib usr/;
foreach my $domain (keys(%domainHash), "default") {
my @files;
if (-e "$newVhostsPath/$domain"){
find { push @files, $File::Find::name if -e } "$newVhostsPath/$domain";
FILE: foreach my $file (@files) {
next FILE if (-d $file or # Skip directories
-S $file or # and sockets (e.g. .plesk/php-fpm.sock)
-B $file or # and binary files.
-l $file); # I'm not sure that I want to follow symlinks.
# Also skip httpd autogenerated configs except vhost.conf and vhost_ssl.conf
next FILE if ( $file =~ m{^$newVhostsPath/$domain/.plesk/conf/}
and $file !~ m{^$newVhostsPath/$domain/.plesk/conf/(vhost|vhost_ssl)\.conf$} );
foreach my $skip_d (@skip_dirs) {
next FILE if $file =~ m{^$newVhostsPath/$domain/$skip_d/};
}
correctFile($file);
}
}
}
return 0;
}
sub correctPhpFpmPools {
print "Correct php-fpm pools configuration...\n";
my $cmd = $config{PRODUCT_ROOT_D} . "/admin/sbin/php_handlers_control --list-json";
my $php_handlers_json = qx{$cmd};
if ($?) {
print STDERR "Failed to read list of php handlers ($?). Continue.";
return;
}
my $handlers_data_ref = decode_json $php_handlers_json;
unless($handlers_data_ref) {
print STDERR "Failed to parse json data: $php_handlers_json. Continue.";
return;
}
my @fpms = grep {$_->{type} eq "fpm" } @{$handlers_data_ref->{php}};
for my $fpm(@fpms) {
correctPhpFpm($fpm->{service}, $fpm->{poold});
}
}
sub correctPhpFpm {
my ($service, @pool_dirs) = @_;
my $configuration_changed = 0;
for my $fpm_dir(@pool_dirs) {
next unless -d $fpm_dir;
opendir(my $dh, $fpm_dir) or die "Cannot open directory: $! : $fpm_dir .\n";
while (defined( my $file = readdir($dh) )) {
next unless $file =~ /(?<!^www)\.conf$/;
correctFile("$fpm_dir/$file");
$configuration_changed = 1;
}
closedir($dh);
}
if ($configuration_changed) {
my $fpm_service_name = $service || "php-fpm";
print "Restart $fpm_service_name service...\n";
my $command = $config{PRODUCT_ROOT_D}."/admin/sbin/pleskrc \"$service\" try-reload";
system($command);
}
}
sub correctPhpIniFiles {
foreach my $domain (keys %statHash) {
my $phpIniFile = $newVhostsPath."/system/".$domain."/etc/php.ini";
next unless -f $phpIniFile;
correctFile($phpIniFile);
}
}
sub setDirtyFlag {
my $packagemng = $config{PRODUCT_ROOT_D} . "/admin/sbin/packagemng";
system("$packagemng --set-dirty-flag");
}
sub fail2BanInstalled {
my $packagemng = $config{PRODUCT_ROOT_D} . "/admin/sbin/packagemng";
my @f2b_status = grep { /^fail2ban:\S+\s*$/ } `$packagemng --list`;
return scalar(@f2b_status) > 0;
}
sub corrertFail2BanApacheJails {
print "Correct Fail2Ban jails configuration...\n";
my $f2bmng = $config{PRODUCT_ROOT_D} . "/admin/sbin/f2bmng";
my @jails = ('plesk-apache', 'plesk-apache-badbot');
JAIL: for my $jail ( @jails ) {
open my $f2b_get, "$f2bmng --get-jail=$jail |" or do {
print "Unable to get jail $jail configuration: $!";
next JAIL
};
my $jail_config = <$f2b_get>;
close $f2b_get;
$jail_config =~ s#(\s"|\\n)\Q$oldVhostsPath\E/#$1$newVhostsPath/#g;
open my $f2b_set, "| $f2bmng --set-jail=$jail" or do {
print "Unable to set jail $jail configuration: $!";
next JAIL
};
print $f2b_set $jail_config;
close $f2b_set;
}
}
sub detectFileSystem {
my %pseudofs = ('autofs' => 1,
'binfmt_misc' => 1,
'cd9660' => 1,
'devfs' => 1,
'devpts' => 1,
'fdescfs' => 1,
'iso9660' => 1,
'linprocfs' => 1,
'proc' => 1,
'procfs' => 1,
'romfs' => 1,
'sysfs' => 1,
'tmpfs' => 1,
'usbdevfs' => 1,
'usbfs' => 1,
'rpc_pipefs' => 1,
);
my $mkdir = `mkdir -p $newVhostsPath` if (! -d "$newVhostsPath");
my %partitions;
foreach my $mountinfo (`mount`) {
chomp $mountinfo;
#unable to use 'undef' here - perl 5.004 compatibility
my ($device, $undef, $mountpoint, $undef1, $type, $options) = split /\s+/, $mountinfo;
my $mode = 'rw';
$mode = 'ro' if ($options =~ /[(,]ro[,)]/);
unless (defined $pseudofs{$type}) {
$partitions{$mountpoint} = ();
$partitions{$mountpoint}->{'device'} = $device;
$partitions{$mountpoint}->{'mode'} = $mode;
$partitions{$mountpoint}->{'type'} = $type;
}
}
foreach my $dfinfo (`LANG=C POSIXLY_CORRECT= df -Pk | tail -n +2`) {
chomp $dfinfo;
#unable to use 'undef' here - perl 5.004 compatibility
my ($undef, $size, $undef1, $free, $undef2, $mountpoint) = split /\s+/, $dfinfo;
if (exists $partitions{$mountpoint}) {
# for brain-dead NFS shares:
$free = $size if $free > $size;
$partitions{$mountpoint}->{'size'} = $size;
$partitions{$mountpoint}->{'free'} = $free;
}
}
my $buf = `LANG=C POSIXLY_CORRECT= df -P $oldVhostsPath | tail -n +2`;
my ($undef, $oldmountpoint, $newmountpoint);
($undef, $undef, $undef, $undef, $undef, $oldmountpoint) = split /\s+/, $buf;
$buf = `LANG=C POSIXLY_CORRECT= df -P $newVhostsPath | tail -n +2`;
($undef, $undef, $undef, $undef, $undef, $newmountpoint) = split /\s+/, $buf;
if ($oldmountpoint ne $newmountpoint){
my $du = `du -k $oldVhostsPath | tail -n 1`;
chomp $du;
my ($oldSize,$undef) = split /\s+/,$du;
if (!exists $partitions{$newmountpoint}) {
print "Partition $newmountpoint uses unsupported filesystem.\n";
exit 1;
} elsif ($oldSize < $partitions{$newmountpoint}->{'free'}) {
correctFileSystem();
}else{
print "Partition $newmountpoint has not enough free space.\n";
exit 1;
}
}else{
correctFileSystem();
}
}
sub getArguments {
my ($ptrArgv,$ptrOpts) = @_;
my (@keys,$firstKey,%prepKeys,$key,$value,$ptrArr,$arg,$state);
my (%retHash,$pat,$found,$used,@rest,$fullArg,$prevKey);
while (($key,$value)=each(%{$ptrOpts})){
@keys = split(/\|/,$key);
$firstKey = $keys[0];
$firstKey =~s/^-*//;
push @{$prepKeys{$firstKey}},[@keys];
push @{$prepKeys{$firstKey}},$value;
}
$state =0;
foreach $arg (@{$ptrArgv}){
$used = 0;
if($state==1){
if ($arg =~ /^-/){
$state=0;
}else{
$used = 1;
$retHash{$prevKey}=$arg;
next;
}
}
if ($state == 2){
$retHash{$prevKey}=$arg;
$state = 0;
$used = 1;
}else{
$fullArg = $arg;
if ($arg =~ /^(-\S+)=(.+)/s){
$arg = $1;
$value = $2;
}else{
$value = undef;
}
foreach $key (keys %prepKeys){
$ptrArr = $prepKeys{$key};
$found = 0;
foreach $pat (@{$ptrArr->[0]}){
if ($pat eq $arg){
$found = 1;
last;
}
}
if($found){
$used = 1;
if(defined($value)){
$retHash{$key}=$value;
}else{
if($ptrArr->[1]){
if($ptrArr->[1] =~ /\?$/){
$state = 1;
}else{
$state = 2;
}
$prevKey = $key;
}else{
$retHash{$key}=undef;
}
}
last;
}
}
}
unless($used){
if($value){
push @rest,$fullArg;
}else{
push @rest,$arg;
}
}
}
@{$ptrArgv}=@rest;
return \%retHash;
}
sub printHelp {
my ($progname)=@_;
my $help = <<HELP;
Utility to transfer vhosts content from HTTPD_VHOSTS_D directory to new place.
Usage:
--dest-dir <path> Destination path. Path to new vhosts directory.
Example: /path/to/new/vhosts.
If directory does not exist it will be created.
--correct-scripts Changes user scripts.
Old vhost path is replaced to new path in content of all files.
HELP
printf($help);
}
# vim:set et ts=4 sts=4 sw=4: