1
0
mirror of https://github.com/OpenNebula/one.git synced 2024-12-23 17:33:56 +03:00

F #4109: add onezone serversync command

This commit is contained in:
Alejandro Huertas 2020-01-21 17:24:52 +01:00 committed by Ruben S. Montero
parent 5ba128ee70
commit 23c6907d89
4 changed files with 468 additions and 3 deletions

View File

@ -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

View File

@ -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',

View File

@ -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={

View File

@ -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