#!/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 () { 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 (){ 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 (){ 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 =~ /(? 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 = < 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: