#!/opt/brepo/ruby33/bin/ruby # info: utility to prepare existing server with hestiacp to use bunkerweb # do not run this script n the server, where bunkerweb was installed with hestiacp # installation # options: COMMAND # # example: v-bunkerweb-migrate migrate-nginx # # Commands: # migratenginx - move old nginx configs to the new port and path # migratetobunkerweb - create items of sites in the bunkerweb database # #------------------------------------------# # Variables & Functions # #------------------------------------------# # Argument definition v_command = ARGV[0] require "/usr/local/hestia/func_ruby/global_options" load_ruby_options_defaults $HESTIA = load_hestia_default_path_from_env require "main" require "modules" require "HestiaBunkerWebApi" require 'json' unless defined?(JSON) require 'fileutils' require 'time' require 'pathname' def copy_nginx_files(src_root, dest_root) FileUtils.mkdir_p(dest_root) Dir.foreach(src_root) do |entry| next if entry == '.' || entry == '..' next if entry == 'modules' || entry == 'modules-enabled' src_path = File.join(src_root, entry) dest_path = File.join(dest_root, entry) if File.directory?(src_path) FileUtils.mkdir_p(dest_path) copy_nginx_files(src_path, dest_path) else FileUtils.cp(src_path, dest_path) end end end # Function to log and execute migration stages def log_migrate_stage(stage) log_file = '/usr/local/hestia/log/bunkerweb_migrate_stages.log' # Check if the stage has already been recorded if File.exist?(log_file) && File.readlines(log_file).any? { |line| line.strip == stage } hestia_print_error_message_to_cli "Stage #{stage} already completed, skipping." return end # Execute the stage block begin yield # Record the successful stage File.open(log_file, 'a') { |f| f.puts stage } hestia_print_info_message_to_cli "Stage #{stage} completed." rescue => e hestia_print_error_message_to_cli "Stage #{stage} failed: #{e.message}" exit 1 end end hestia_check_privileged_user load_global_bash_variables "/etc/hestiacp/hestia.conf" if $HESTIA.nil? hestia_print_error_message_to_cli "Can't find HESTIA base path" exit 1 end load_global_bash_variables "#{$HESTIA}/conf/hestia.conf" #------------------------------------------# # Verifications # #------------------------------------------# check_args 1, ARGV, "COMMAND" # Perform verification if read-only mode is enabled check_hestia_demo_mode #------------------------------------------# # Action # #------------------------------------------# case v_command.to_sym when :migratenginx log_migrate_stage('stage0') do # Create backup of /etc/nginx with timestamp timestamp = Time.now.strftime('%Y%m%d_%H%M%S') backup_root = "/etc/nginx_backup_#{timestamp}" FileUtils.mkdir_p(backup_root) src_root = '/etc/nginx' dest_root = backup_root # Custom copy function to handle symlinks in conf.d/domains def copy_with_symlinks(src, dest) Dir.foreach(src) do |entry| next if entry == '.' || entry == '..' src_path = File.join(src, entry) dest_path = File.join(dest, entry) if File.symlink?(src_path) # Check if symlink is inside conf.d/domains if src_path.include?(File.join('conf.d', 'domains')) # Resolve the target of the symlink target_path = File.readlink(src_path) # Resolve relative symlink paths unless Pathname.new(target_path).absolute? target_path = File.expand_path(target_path, File.dirname(src_path)) end if File.exist?(target_path) && File.file?(target_path) content = File.read(target_path) new_file_name = "#{entry}_content.conf" new_file_path = File.join(dest, new_file_name) File.write(new_file_path, content) end else # Preserve the symlink as is FileUtils.mkdir_p(File.dirname(dest_path)) target = File.readlink(src_path) FileUtils.ln_s(target, dest_path) end elsif File.directory?(src_path) FileUtils.mkdir_p(dest_path) copy_with_symlinks(src_path, dest_path) else FileUtils.cp(src_path, dest_path) end end end copy_with_symlinks(src_root, dest_root) end log_migrate_stage('stage1') do if $BUNKERWEB.nil? hestia_change_sys_config_value("BUNKERWEB", "yes") end end log_migrate_stage('stage2') do unless system('yum install -y nginx-system') hestia_print_error_message_to_cli "Failed to install nginx-system via yum" log_event E_ARGS, $ARGUMENTS exit 1 end end log_migrate_stage('stage3') do FileUtils.rm_f Dir.glob('/usr/local/hestia/nginx-system/etc/nginx/conf.d/*.conf') src_root = '/etc/nginx' dest_root = '/usr/local/hestia/nginx-system/etc/nginx' copy_nginx_files(src_root, dest_root) end log_migrate_stage('stage4') do # Find all files under the nginx-system directory nginx_conf_dir = '/usr/local/hestia/nginx-system/etc/nginx' Dir.glob(File.join(nginx_conf_dir, '**', '*')).each do |path| hestia_print_info_message_to_cli "stage 4 processing file #{path}" next if File.directory?(path) content = File.read(path) new_content = content.gsub(/(?:#{$PROXY_PORT} [flags]; # Matches: listen 192.168.3.81:80; or listen 192.168.3.81:80 default_server; new_content = content.gsub(/(?:#{$PROXY_SSL_PORT} ssl; if new_content == content new_content = content.gsub(/(? e hestia_print_error_message_to_cli "Failed to parse JSON from state command: #{e.message}" exit 1 end else hestia_print_error_message_to_cli "Failed to enable update_module" exit 1 end end log_migrate_stage('stage8') do hestia_change_sys_config_value("PROXY_PORT", "8078") hestia_change_sys_config_value("PROXY_SSL_PORT", "8079") end #stage8 активация из запуск nginx-system log_migrate_stage('stage9') do # Delete all contents inside /etc/nginx FileUtils.rm_rf Dir.glob('/etc/nginx/*') # Stop nginx service system('systemctl stop nginx') # Start nginx-system service system('systemctl start nginx-system') end when :migratetobunkerweb log_migrate_stage('stage10') do if system('/usr/local/hestia/bin/v-ext-modules enable bunkerweb_module') output = IO.popen("/usr/local/hestia/bin/v-ext-modules state bunkerweb_module json").read begin parsed = JSON.parse(output) if parsed.is_a?(Array) && parsed.first && parsed.first['STATE'] == 'enabled' result = system('/usr/local/hestia/bin/v-ext-modules-run bunkerweb_module configure') unless result hestia_print_error_message_to_cli "bunkerweb_module configure command failed" exit 1 end else hestia_print_error_message_to_cli "bunkerweb_module not enabled after enable command" exit 1 end rescue JSON::ParserError => e hestia_print_error_message_to_cli "Failed to parse JSON from state command: #{e.message}" exit 1 end else hestia_print_error_message_to_cli "Failed to enable bunkerweb_module" exit 1 end end LIST_DOMAINS=[] log_migrate_stage('stage11') do hestia_print_info_message_to_cli "Stage 11: Migrating users and domains to BunkerWeb..." # Get all users from HestiaCP in JSON format user_list_output = IO.popen("/usr/local/hestia/bin/v-list-users json").read begin user_data = JSON.parse(user_list_output) rescue JSON::ParserError => e hestia_print_error_message_to_cli "Failed to parse users JSON: #{e.message}" exit 1 end # Iterate over each user user_data.each do |username, user_info| next unless user_info.is_a?(Hash) web_domains_count = user_info['U_WEB_DOMAINS'] next unless web_domains_count && web_domains_count.to_i > 0 hestia_print_info_message_to_cli "Processing user: #{username} (#{web_domains_count} domains)" # Get web domains for this user in JSON format domain_list_output = IO.popen("/usr/local/hestia/bin/v-list-web-domains #{username} json").read begin domain_data = JSON.parse(domain_list_output) rescue JSON::ParserError => e hestia_print_error_message_to_cli "Failed to parse domains JSON for user #{username}: #{e.message}" next end # Process each domain domain_data.each do |domain_name, domain_info| next unless domain_info.is_a?(Hash) ssl_status = domain_info['SSL'] || 'no' is_ssl = ssl_status == 'yes' # Create alias list from domain info (ALIAS field contains comma-separated aliases) raw_aliases = domain_info['ALIAS'] || '' if raw_aliases && !raw_aliases.empty? # Hestia uses comma-separated, pass as-is to v-bunkerweb-module (it handles conversion internally) aliases_list = raw_aliases else aliases_list = domain_name # No aliases, use domain name only end hestia_print_info_message_to_cli "Processing domain: #{domain_name} (SSL: #{is_ssl}, Aliases: #{aliases_list})" begin # Add domain to BunkerWeb via module script (without SSL) output = IO.popen("/usr/local/hestia/bin/v-bunkerweb-module add #{domain_name} #{$PROXY_HOST}") hestia_print_info_message_to_cli " Added #{domain_name}: #{output.strip}" rescue => e hestia_print_error_message_to_cli "Failed to add domain #{domain_name} to BunkerWeb: #{e.message}" next end # Add aliases to the domain via module script begin output = IO.popen("/usr/local/hestia/bin/v-bunkerweb-module alias #{domain_name} \"#{aliases_list}\"") hestia_print_info_message_to_cli " Added aliases for #{domain_name}: #{output.strip}" rescue => e hestia_print_error_message_to_cli "Failed to set aliases for #{domain_name}: #{e.message}" next end # Handle SSL configuration if enabled if is_ssl original_ssl_dir = "/home/#{username}/conf/#{domain_name}/web/ssl" # Check if SSL directory and files exist unless Dir.exist?(original_ssl_dir) || File.exist?("#{original_ssl_dir}/#{domain_name}.pem") hestia_print_error_message_to_cli "Warning: SSL files not found for #{domain_name}" LIST_DOMAINS << { user: username, domain: domain_name, is_ssl: true, path_to_ssl: nil } next end # Define bunkerweb directory for this domain's certificates bunkerweb_ssl_dir = "/home/#{username}/conf/web/#{domain_name}/ssl/bunkerweb" begin # Create bunkerweb SSL directory if it doesn't exist FileUtils.mkdir_p(bunkerweb_ssl_dir) Dir.chmod(bunkerweb_ssl_dir, 0755) # Define paths in bunkerweb directory cert_path_bunkerweb = "#{bunkerweb_ssl_dir}/#{domain_name}.pem" key_path_bunkerweb = "#{bunkerweb_ssl_dir}/#{domain_name}.key" # Copy all SSL files if they exist (matching v-add-web-domain-ssl behavior) original_crt = "#{original_ssl_dir}/#{domain_name}.crt" original_pem = "#{original_ssl_dir}/#{domain_name}.pem" original_key = "#{original_ssl_dir}/#{domain_name}.key" original_ca = "#{original_ssl_dir}/#{domain_name}.ca" # Copy .crt file if exists if File.exist?(original_crt) FileUtils.cp("-f", original_crt, cert_path_bunkerweb) elsif File.exist?(original_pem) # Only use .pem if .crt doesn't exist (fallback like the backup script) FileUtils.cp("-f", original_pem, cert_path_bunkerweb) end # Copy .key file if exists if File.exist?(original_key) FileUtils.cp("-f", original_key, key_path_bunkerweb) end # Copy .pem file if exists (in addition to .crt for backup purposes) if File.exist?(original_pem) FileUtils.cp("-f", original_pem, cert_path_bunkerweb) end # Copy .ca file if exists if File.exist?(original_ca) FileUtils.cp("-f", original_ca, "#{bunkerweb_ssl_dir}/#{domain_name}.ca") end # Set ownership and permissions (nginx user can read, others cannot) Dir.chown(bunkerweb_ssl_dir, 'root', 'nginx') Dir.chmod(bunkerweb_ssl_dir, 0755) File.chown(cert_path_bunkerweb, 'root', 'nginx') if File.exist?(cert_path_bunkerweb) File.chmod(cert_path_bunkerweb, 0640) if File.exist?(cert_path_bunkerweb) File.chown(key_path_bunkerweb, 'root', 'nginx') if File.exist?(key_path_bunkerweb) File.chmod(key_path_bunkerweb, 0640) if File.exist?(key_path_bunkerweb) # Add SSL configuration to BunkerWeb via module script output = IO.popen("/usr/local/hestia/bin/v-bunkerweb-module addssl #{domain_name} #{cert_path_bunkerweb} #{key_path_bunkerweb}") hestia_print_error_message_to_cli " Added SSL for #{domain_name}: #{output.strip}" # Update path_to_ssl to point to bunkerweb directory ssl_cert_path = cert_path_bunkerweb rescue => e hestia_print_error_message_to_cli "Failed to configure SSL for #{domain_name}: #{e.message}" end end # Populate LIST_DOMAINS array with domain info LIST_DOMAINS << { user: username, domain: domain_name, is_ssl: is_ssl, path_to_ssl: ssl_cert_path ? ssl_cert_path : nil } hestia_print_info_message_to_cli "Successfully migrated #{domain_name} for user #{username}" end end # Output the populated LIST_DOMAINS array if !LIST_DOMAINS.empty? hestia_print_info_message_to_cli "\n=== Populated LIST_DOMAINS ===" LIST_DOMAINS.each_with_index do |entry, idx| hestia_print_info_message_to_cli "#{idx + 1}. user: #{entry[:user]}, domain: #{entry[:domain]}, is_ssl: #{entry[:is_ssl].to_s}, path_to_ssl: #{entry[:path_to_ssl]}" end # Output as JSON for potential use in downstream scripts hestia_print_info_message_to_cli "\n=== LIST_DOMAINS (JSON) ===" hestia_print_info_message_to_cli JSON.generate(LIST_DOMAINS, { indent: 2 }) else hestia_print_error_message_to_cli "No domains were migrated to BunkerWeb" end hestia_print_info_message_to_cli "Stage 11 completed." end else hestia_print_error_message_to_cli "unknown command" log_event E_ARGS, $ARGUMENTS exit 1 end exit 0