From 23c6907d89fd62960e346233fea8803d39f75d98 Mon Sep 17 00:00:00 2001 From: Alejandro Huertas Date: Tue, 21 Jan 2020 17:24:52 +0100 Subject: [PATCH] F #4109: add onezone serversync command --- share/install_gems/Gemfile | 1 + share/install_gems/install_gems | 9 +- src/cli/one_helper/onezone_helper.rb | 426 +++++++++++++++++++++++++++ src/cli/onezone | 35 +++ 4 files changed, 468 insertions(+), 3 deletions(-) diff --git a/share/install_gems/Gemfile b/share/install_gems/Gemfile index 1a09046b68..13306e29aa 100644 --- a/share/install_gems/Gemfile +++ b/share/install_gems/Gemfile @@ -73,3 +73,4 @@ gem 'activesupport', '~> 4.2' # packethost gem 'i18n', '~> 0.9' # packethost gem 'ffi-rzmq', '~> 2.0.7' # onehem (hooks) gem 'ipaddress', '~> 0.8.3' # sunstone, oneflow +gem 'augeas', '~> 0.6' # serversync diff --git a/share/install_gems/install_gems b/share/install_gems/install_gems index 9ecdb563fd..add8a99b42 100755 --- a/share/install_gems/install_gems +++ b/share/install_gems/install_gems @@ -47,7 +47,8 @@ GROUPS={ :oca => 'ox', :market => 'aws-sdk', :onedb => "mysql2", - :hooks => "zeromq" + :hooks => "zeromq", + :serversync => "augeas" } PACKAGES=GROUPS.keys @@ -69,7 +70,8 @@ DISTRIBUTIONS={ 'xmlparser' => ['gcc', 'libexpat1-dev'], 'thin' => ['g++'], 'json' => ['gcc'], - 'zeromq' => ['gcc', 'libzmq5', 'libzmq3-dev'] + 'zeromq' => ['gcc', 'libzmq5', 'libzmq3-dev'], + 'augeas' => ['gcc', 'libaugeas-dev'] }, :install_command_interactive => 'apt-get install', :install_command => 'apt-get -y install', @@ -89,7 +91,8 @@ DISTRIBUTIONS={ 'xmlparser' => ['gcc', 'expat-devel'], 'thin' => ['gcc-c++'], 'json' => ['gcc'], - 'zeromq' => ['gcc', 'zeromq', 'zeromq-devel'] + 'zeromq' => ['gcc', 'zeromq', 'zeromq-devel'], + 'augeas' => ['gcc', 'augeas-devel'] }, :install_command_interactive => 'yum install', :install_command => 'yum -y install', diff --git a/src/cli/one_helper/onezone_helper.rb b/src/cli/one_helper/onezone_helper.rb index acb6e538fb..87eef30786 100644 --- a/src/cli/one_helper/onezone_helper.rb +++ b/src/cli/one_helper/onezone_helper.rb @@ -14,8 +14,434 @@ # limitations under the License. # #--------------------------------------------------------------------------- # +require 'fileutils' +require 'tempfile' +require 'CommandManager' + require 'one_helper' +# Check differences between files and copy them +class Replicator + + SSH_OPTIONS = '-o stricthostkeychecking=no -o passwordauthentication=no' + ONE_AUTH = '/var/lib/one/.one/one_auth' + FED_ATTRS = %w[MODE ZONE_ID SERVER_ID MASTER_ONED] + + FILES = [ + { :name => 'az_driver.conf', + :service => 'opennebula' }, + { :name => 'az_driver.default', + :service => 'opennebula' }, + { :name => 'ec2_driver.conf', + :service => 'opennebula' }, + { :name => 'ec2_driver.default', + :service => 'opennebula' }, + { :name => 'econe.conf', + :service => 'opennebula-econe' }, + { :name => 'oneflow-server.conf', + :service => 'opennebula-flow' }, + { :name => 'onegate-server.conf', + :service => 'opennebula-gate' }, + { :name => 'sched.conf', + :service => 'opennebula' }, + { :name => 'sunstone-logos.yaml', + :service => 'opennebula-sunstone' }, + { :name => 'sunstone-server.conf', + :service => 'opennebula-sunstone' }, + { :name => 'vcenter_driver.default', + :service => 'opennebula' } + ] + + FOLDERS = [ + { :name => 'sunstone-views', :service => 'opennebula-sunstone' }, + { :name => 'auth', :service => 'opennebula' }, + { :name => 'ec2query_templates', :service => 'opennebula' }, + { :name => 'hm', :service => 'opennebula' }, + { :name => 'sunstone-views', :service => 'opennebula' }, + { :name => 'vmm_exec', :service => 'opennebula' } + ] + + # Class constructor + # + # @param ssh_key [String] SSH key file path + # @param server [String] OpenNebula server IP address + def initialize(ssh_key, server) + @oneadmin_identity_file = ssh_key + @remote_server = server + + # Get local configuration + l_credentials = File.read(ONE_AUTH).gsub("\n", '') + l_endpoint = 'http://localhost:2633/RPC2' + local_client = Client.new(l_credentials, l_endpoint) + + @l_config = OpenNebula::System.new(local_client).get_configuration + @l_config_elements = { :raw => @l_config } + @l_fed_elements = { :raw => @l_config } + + fetch_db_config(@l_config_elements) + fetch_fed_config(@l_fed_elements) + + # Get remote configuration + r_credentials = ssh("cat #{ONE_AUTH}").stdout.gsub("\n", '') + r_endpoint = "http://#{server}:2633/RPC2" + remote_client = Client.new(r_credentials, r_endpoint) + + @r_config = OpenNebula::System.new(remote_client).get_configuration + @r_config_elements = { :raw => @r_config } + @r_fed_elements = { :raw => @r_config } + + fetch_db_config(@r_config_elements) + fetch_fed_config(@r_fed_elements) + + # Set OpenNebula services to not restart + @opennebula_services = { 'opennebula' => false, + 'opennebula-sunstone' => false, + 'opennebula-gate' => false, + 'opennebula-flow' => false, + 'opennebula-econe' => false } + end + + # Process files and folders + # + # @param sync_db [Boolean] True to sync database + def process_files(sync_db) + # Files to be copied + copy_onedconf + + FILES.each do |file| + copy_and_check(file[:name], file[:service]) + end + + # Folders to be copied + FOLDERS.each do |folder| + copy_folder(folder[:name], folder[:service]) + end + + restart_services + + # Sync database + sync_db + end + + private + + # Get database configuration + # + # @param configs [Object] Configuration + def fetch_db_config(configs) + configs.store(:backend, configs[:raw]['/TEMPLATE/DB/BACKEND']) + + if configs[:backend] == 'mysql' + configs.store(:server, configs[:raw]['/TEMPLATE/DB/SERVER']) + configs.store(:user, configs[:raw]['/TEMPLATE/DB/USER']) + configs.store(:password, configs[:raw]['/TEMPLATE/DB/PASSWD']) + configs.store(:dbname, configs[:raw]['/TEMPLATE/DB/DB_NAME']) + configs.store(:port, configs[:raw]['/TEMPLATE/DB/PORT']) + configs[:port] = '3306' if configs[:port] == '0' + else + STDERR.puts 'No mysql backend configuration found' + exit(-1) + end + end + + # Get federation configuration + # + # @param configs [Object] Configuration + def fetch_fed_config(configs) + configs.store(:server_id, + configs[:raw]['/TEMPLATE/FEDERATION/SERVER_ID']) + configs.store(:zone_id, + configs[:raw]['/TEMPLATE/FEDERATION/ZONE_ID']) + end + + # Replaces a file with the version located on a remote server + # Only replaces the file if it's different from the remote one + # + # @param file [String] File to check + # @param service [String] Service to restart + def copy_and_check(file, service) + puts "Checking #{file}" + + temp_file = Tempfile.new("#{file}-temp") + + scp("/etc/one/#{file}", temp_file.path) + + if !FileUtils.compare_file(temp_file, "/etc/one/#{file}") + FileUtils.cp(temp_file.path, "/etc/one/#{file}") + + puts "#{file} has been replaced by #{@remote_server}:#{file}" + + @opennebula_services[service] = true + end + ensure + temp_file.unlink + end + + # Copy folders + # + # @param folder [String] Folder to copy + # @param service [String] Service to restart + def copy_folder(folder, service) + puts "Checking #{folder}" + + rc = run_command( + "rsync -ai\ + -e \"ssh #{SSH_OPTIONS} -i #{@oneadmin_identity_file}\" " \ + "#{@remote_server}:/etc/one/#{folder}/ " \ + "/etc/one/#{folder}/" + ) + + unless rc + rc = run_command( + "rsync -ai\ + -e \"ssh #{SSH_OPTIONS} -i #{@oneadmin_identity_file}\" " \ + "oneadmin@#{@remote_server}:/etc/one/#{folder}/ " \ + "/etc/one/#{folder}/" + ) + end + + unless rc + STDERR.puts 'ERROR' + STDERR.puts "Fail to sync #{folder}" + exit(-1) + end + + output = rc.stdout + + return if output.empty? + + puts "Folder #{folder} has been sync with #{@remote_server}:#{folder}" + + @opennebula_services[service] = true + end + + # oned.conf file on distributed environments will always be different, + # due to the federation section. + # Replace oned.conf based on a remote server's version maintaining + # the old FEDERATION section + def copy_onedconf + puts 'Checking oned.conf' + + # Create temporarhy files + l_oned = Tempfile.new('l_oned') + r_oned = Tempfile.new('r_oned') + + l_oned.close + r_oned.close + + # Copy remote and local oned.conf files to temporary files + scp('/etc/one/oned.conf', r_oned.path) + + FileUtils.cp('/etc/one/oned.conf', l_oned.path) + + # Create augeas objects to manage oned.conf files + l_work_file_dir = File.dirname(l_oned.path) + l_work_file_name = File.basename(l_oned.path) + + r_work_file_dir = File.dirname(r_oned.path) + r_work_file_name = File.basename(r_oned.path) + + l_aug = Augeas.create(:no_modl_autoload => true, + :no_load => true, + :root => l_work_file_dir, + :loadpath => l_oned.path) + + l_aug.clear_transforms + l_aug.transform(:lens => 'Oned.lns', :incl => l_work_file_name) + l_aug.context = "/files/#{l_work_file_name}" + l_aug.load + + r_aug = Augeas.create(:no_modl_autoload => true, + :no_load => true, + :root => r_work_file_dir, + :loadpath => r_oned.path) + + r_aug.clear_transforms + r_aug.transform(:lens => 'Oned.lns', :incl => r_work_file_name) + r_aug.context = "/files/#{r_work_file_name}" + r_aug.load + + # Get local federation information + fed_attrs = [] + + FED_ATTRS.each do |attr| + fed_attrs << l_aug.get("FEDERATION/#{attr}") + end + + # Remove federation section + l_aug.rm('FEDERATION') + r_aug.rm('FEDERATION') + + # Save augeas files in temporary directories + l_aug.save + r_aug.save + + return if FileUtils.compare_file(l_oned.path, r_oned.path) + + time_based_identifier = Time.now.to_i + + # backup oned.conf + FileUtils.cp('/etc/one/oned.conf', + "/etc/one/oned.conf#{time_based_identifier}") + + FED_ATTRS.zip(fed_attrs) do |name, value| + r_aug.set("FEDERATION/#{name}", value) + end + + r_aug.save + + FileUtils.cp(r_oned.path, '/etc/one/oned.conf') + + puts 'oned.conf has been replaced by ' \ + "#{@remote_server}:/etc/one/oned.conf" + + puts 'A copy of your old oned.conf file is located here: ' \ + "/etc/one/oned.conf#{time_based_identifier}" + + @opennebula_services['opennebula'] = true + end + + # Sync database + def sync_db + puts "Dumping and fetching database from #{@remote_server}, " \ + 'this could take a while' + + ssh( + "onedb backup -f -u #{@r_config_elements[:user]} " \ + "-p #{@r_config_elements[:password]} " \ + "-d #{@r_config_elements[:dbname]} " \ + "-P #{@r_config_elements[:port]} /tmp/one_db_dump.sql" + ) + + scp('/tmp/one_db_dump.sql', '/tmp/one_db_dump.sql') + + puts "Local OpenNebula's database will be replaced, hang tight" + + service_action('opennebula', 'stop') + + puts 'Restoring database' + + run_command( + "onedb restore -f -u #{@l_config_elements[:user]} " \ + "-p #{@l_config_elements[:password]} " \ + "-d #{@l_config_elements[:dbname]} " \ + "-P #{@l_config_elements[:port]} " \ + "-h #{@l_config_elements[:server]}" \ + '/tmp/one_db_dump.sql', + true + ) + + service_action('opennebula', 'start') + end + + # Run local command + # + # @param cmd [String] Command to run + # @param print_output [Boolean] True to show output + def run_command(cmd, print_output = false) + output = LocalCommand.run(cmd) + + if output.code == 0 + output + else + return false unless print_output + + STDERR.puts 'ERROR' + STDERR.puts "Failed to run: #{cmd}" + STDERR.puts output.stderr + + false + end + end + + # Execute SSH command + # + # @param cmd [String] Command to execute + def ssh(cmd) + rc = run_command( + "ssh -i #{@oneadmin_identity_file} " \ + "#{SSH_OPTIONS} #{@remote_server} " \ + "#{cmd}" + ) + + # if default users doesn't work, try with oneadmin + unless rc + rc = run_command( + "ssh -i #{@oneadmin_identity_file} " \ + "#{SSH_OPTIONS} oneadmin@#{@remote_server} " \ + "#{cmd}" + ) + end + + # if oneadmin doesn't work neither, fail + unless rc + STDERR.puts 'ERROR' + STDERR.puts "Couldn't execute command #{cmd} on remote host" + exit(-1) + end + + rc + end + + # Execute SCP command + # + # @param src [String] Source path + # @param dest [String] Destination path + def scp(src, dest) + rc = run_command( + "scp -i #{@oneadmin_identity_file} " \ + "#{SSH_OPTIONS} #{@remote_server}:/#{src} #{dest}" + ) + + # if default users doesn't work, try with oneadmin + unless rc + rc = run_command( + "scp -i #{@oneadmin_identity_file} " \ + "#{SSH_OPTIONS} oneadmin@#{@remote_server}:#{src} #{dest}" + ) + end + + # if oneadmin doesn't work neither, fail + unless rc + STDERR.puts 'ERROR' + STDERR.puts "Couldn't execute command #{cmd} on remote host" + exit(-1) + end + end + + # Restart OpenNebula services + def restart_services + restarted = false + + @opennebula_services.each do |service, status| + next unless status + + service_action(service) + + restarted = true + end + + return if restarted + + puts 'Everything seems synchronized, nothing was replaced.' + end + + # Service action + # + # @param service [String] Service to restart + # @param action [String] Action to execute (start, stop, restart) + def service_action(service, action = 'try-restart') + if `file /sbin/init`.include? 'systemd' + puts "#{action}ing #{service} via systemd" + run_command("systemctl #{action} #{service}", true) + else + puts "#{action}ing #{service} via init" + run_command("service #{service} #{action}", true) + end + end + +end + class OneZoneHelper < OpenNebulaHelper::OneHelper SERVER_NAME={ diff --git a/src/cli/onezone b/src/cli/onezone index f9e7ebc0f5..cc662a8496 100755 --- a/src/cli/onezone +++ b/src/cli/onezone @@ -51,6 +51,12 @@ CommandParser::CmdParser.new(ARGV) do :format => String } + DATABASE = { + :name => 'database', + :large => '--db', + :description => 'Also sync database' + } + before_proc do helper.set_client(options) end @@ -215,4 +221,33 @@ CommandParser::CmdParser.new(ARGV) do helper.set_zone(args[0], false) end end + + sync_desc = <<-EOT.unindent + Syncs configuration files and folders from another server + + This command must be executed under root + EOT + + command :serversync, sync_desc, :server, :options => [DATABASE] do + begin + gem 'augeas', '~> 0.6' + require 'augeas' + rescue Gem::LoadError + STDERR.puts( + 'Augeas gem is not installed, run `gem install ' \ + 'augeas -v \'0.6\'` to install it' + ) + exit(-1) + end + + if !Process.uid.zero? || !Process.gid.zero? + STDERR.puts("'onezone serversync' must be run under root") + exit(-1) + end + + server = Replicator.new('/var/lib/one/.ssh/id_rsa', args[0]) + server.process_files(options.key?(:database)) + + 0 + end end