mirror of
https://github.com/OpenNebula/one.git
synced 2025-03-28 14:50:08 +03:00
parent
0aad48afbe
commit
ee683bac6b
30
install.sh
30
install.sh
@ -323,7 +323,11 @@ LIB_DIRS="$LIB_LOCATION/ruby \
|
||||
$LIB_LOCATION/onecfg/lib/config/type \
|
||||
$LIB_LOCATION/onecfg/lib/config/type/augeas \
|
||||
$LIB_LOCATION/onecfg/lib/config/type/yaml \
|
||||
$LIB_LOCATION/onecfg/lib/patch"
|
||||
$LIB_LOCATION/onecfg/lib/patch \
|
||||
$LIB_LOCATION/ruby/onevmdump \
|
||||
$LIB_LOCATION/ruby/onevmdump/lib \
|
||||
$LIB_LOCATION/ruby/onevmdump/lib/exporters \
|
||||
$LIB_LOCATION/ruby/onevmdump/lib/restorers"
|
||||
|
||||
VAR_DIRS="$VAR_LOCATION/remotes \
|
||||
$VAR_LOCATION/remotes/etc \
|
||||
@ -774,6 +778,10 @@ INSTALL_FILES=(
|
||||
CONTEXT_SHARE:$SHARE_LOCATION/context
|
||||
DOCKERFILE_TEMPLATE:$SHARE_LOCATION/dockerhub
|
||||
DOCKERFILES_TEMPLATES:$SHARE_LOCATION/dockerhub/dockerfiles
|
||||
ONEVMDUMP_FILES:$LIB_LOCATION/ruby/onevmdump
|
||||
ONEVMDUMP_LIB_FILES:$LIB_LOCATION/ruby/onevmdump/lib
|
||||
ONEVMDUMP_LIB_EXPORTERS_FILES:$LIB_LOCATION/ruby/onevmdump/lib/exporters
|
||||
ONEVMDUMP_LIB_RESTORERS_FILES:$LIB_LOCATION/ruby/onevmdump/lib/restorers
|
||||
)
|
||||
|
||||
INSTALL_CLIENT_FILES=(
|
||||
@ -984,6 +992,7 @@ BIN_FILES="src/nebula/oned \
|
||||
src/cli/onevntemplate \
|
||||
src/cli/onehook \
|
||||
src/onedb/onedb \
|
||||
src/onevmdump/onevmdump \
|
||||
share/scripts/qemu-kvm-one-gen \
|
||||
share/scripts/one"
|
||||
|
||||
@ -2235,6 +2244,22 @@ ONEDB_FILES="src/onedb/fsck.rb \
|
||||
ONEDB_PATCH_FILES="src/onedb/patches/4.14_monitoring.rb \
|
||||
src/onedb/patches/history_times.rb"
|
||||
|
||||
#-------------------------------------------------------------------------------
|
||||
# onevmdump command, to be installed under $LIB_LOCATION
|
||||
#-------------------------------------------------------------------------------
|
||||
|
||||
ONEVMDUMP_FILES="src/onevmdump/onevmdump.rb"
|
||||
|
||||
ONEVMDUMP_LIB_FILES="src/onevmdump/lib/command.rb \
|
||||
src/onevmdump/lib/commons.rb"
|
||||
|
||||
ONEVMDUMP_LIB_EXPORTERS_FILES="src/onevmdump/lib/exporters/base.rb \
|
||||
src/onevmdump/lib/exporters/file.rb \
|
||||
src/onevmdump/lib/exporters/lv.rb \
|
||||
src/onevmdump/lib/exporters/rbd.rb"
|
||||
|
||||
ONEVMDUMP_LIB_RESTORERS_FILES="src/onevmdump/lib/restorers/base.rb"
|
||||
|
||||
#-------------------------------------------------------------------------------
|
||||
# Configuration files for OpenNebula, to be installed under $ETC_LOCATION
|
||||
#-------------------------------------------------------------------------------
|
||||
@ -3023,7 +3048,8 @@ MAN_FILES="share/man/oneacct.1.gz \
|
||||
share/man/onemarket.1.gz \
|
||||
share/man/onemarketapp.1.gz \
|
||||
share/man/onevmgroup.1.gz \
|
||||
share/man/onevntemplate.1.gz"
|
||||
share/man/onevntemplate.1.gz \
|
||||
share/man/onevmdump.1.gz"
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
# Docs Files
|
||||
|
@ -52,6 +52,7 @@ COMMANDS=(
|
||||
|
||||
'oneflow' 'Manage oneFlow Services'
|
||||
'oneflow-template' 'Manage oneFlow Templates'
|
||||
'onevmdump' 'Dumps VM content'
|
||||
)
|
||||
|
||||
DIR_BUILD=$(mktemp -d)
|
||||
|
@ -825,6 +825,10 @@ module OpenNebula
|
||||
self['DEPLOY_ID']
|
||||
end
|
||||
|
||||
def get_history_record(seq)
|
||||
retrieve_xmlelements('//HISTORY')[seq].to_xml
|
||||
end
|
||||
|
||||
def wait_state(state, timeout=120)
|
||||
require 'opennebula/wait_ext'
|
||||
|
||||
|
146
src/onevmdump/lib/command.rb
Normal file
146
src/onevmdump/lib/command.rb
Normal file
@ -0,0 +1,146 @@
|
||||
# -------------------------------------------------------------------------- #
|
||||
# Copyright 2002-2021, OpenNebula Project, OpenNebula Systems #
|
||||
# #
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may #
|
||||
# not use this file except in compliance with the License. You may obtain #
|
||||
# a copy of the License at #
|
||||
# #
|
||||
# http://www.apache.org/licenses/LICENSE-2.0 #
|
||||
# #
|
||||
# Unless required by applicable law or agreed to in writing, software #
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, #
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. #
|
||||
# See the License for the specific language governing permissions and #
|
||||
# limitations under the License. #
|
||||
#--------------------------------------------------------------------------- #
|
||||
|
||||
require 'open3'
|
||||
|
||||
# Command helper class
|
||||
class Command
|
||||
|
||||
def initialize(remote_user = nil, remote_host = nil)
|
||||
@local = true
|
||||
@host = nil
|
||||
@user = nil
|
||||
|
||||
# Set up host if provided
|
||||
if !remote_host.nil? && !remote_host.empty?
|
||||
@host = remote_host
|
||||
@local = false
|
||||
end
|
||||
|
||||
# Return if no user provided
|
||||
return if remote_user.nil? || remote_user.empty?
|
||||
|
||||
# Fail if user provided but not host
|
||||
raise 'Remote user provided, but host is empty.' unless @host
|
||||
|
||||
@user = remote_user
|
||||
end
|
||||
|
||||
# Executes a command either locally on remotely depending on the
|
||||
# configuration.
|
||||
#
|
||||
# The comand and each arg will be wraped by single quotes to avoid
|
||||
# injections attacks.
|
||||
#
|
||||
# @return [String, String, Process::Status] the standard output,
|
||||
# standard error and
|
||||
# status returned by
|
||||
def run(cmd, *args)
|
||||
if cmd.split.size > 1
|
||||
raise 'Cannot run cmd for security reasons.' \
|
||||
'Check run_insecure method'
|
||||
end
|
||||
|
||||
cmd = "'#{cmd}'"
|
||||
args = quote_args(args)
|
||||
|
||||
run_insecure(cmd, args)
|
||||
end
|
||||
|
||||
# Executes a command either locally on remotely depending on the
|
||||
# configuration. And redirect the stdout and stderr to the path
|
||||
# contained in the corresponding variables
|
||||
#
|
||||
# The comand and each arg will be wraped by single quotes to avoid
|
||||
# injections attacks.
|
||||
#
|
||||
# @return [String, String, Process::Status] the standard output,
|
||||
# standard error and
|
||||
# status returned by
|
||||
def run_redirect_output(cmd, stdout, stderr, *args)
|
||||
cmd = "'#{cmd}'"
|
||||
args = quote_args(args)
|
||||
|
||||
args << "> #{stdout}" if !stdout.nil? && !stdout.empty?
|
||||
args << "2> #{stderr}" if !stderr.nil? && !stderr.empty?
|
||||
|
||||
run_insecure(cmd, args)
|
||||
end
|
||||
|
||||
# Executes a command either locally on remotely depending on the
|
||||
# configuration. And redirect the stdout and stderr to the path
|
||||
# contained in the corresponding variables
|
||||
#
|
||||
# This method won't validate the input, hence the command execution
|
||||
# is prone to injection attacks. Ensure both cmd and arguments have
|
||||
# been validated before using this method and when possible use secure
|
||||
# methods instead.
|
||||
#
|
||||
# @return [String, String, Process::Status] the standard output,
|
||||
# standard error and
|
||||
# status returned by
|
||||
def run_insecure(cmd, *args)
|
||||
if @local
|
||||
run_local(cmd, args)
|
||||
else
|
||||
run_ssh(@user, @host, cmd, args)
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# Executes a command locally
|
||||
# @return [String, String, Process::Status] the standard output,
|
||||
# standard error and
|
||||
# status returned by
|
||||
# Open3.capture3
|
||||
def run_local(cmd, *args)
|
||||
cmd_str = "#{cmd} #{args.join(' ')}"
|
||||
|
||||
Open3.capture3(cmd_str)
|
||||
end
|
||||
|
||||
# Executes a command remotely via SSH
|
||||
# @return [String, String, Process::Status] the standard output,
|
||||
# standard error and
|
||||
# status returned by
|
||||
# Open3.capture3
|
||||
def run_ssh(user, host, cmd, *args)
|
||||
ssh_usr = ''
|
||||
|
||||
ssh_usr = "-l \"#{user}\"" if !user.nil? && !user.empty?
|
||||
|
||||
# TODO, should we make this configurabe?
|
||||
ssh_opts = '-o ForwardAgent=yes -o ControlMaster=no ' \
|
||||
'-o ControlPath=none -o StrictHostKeyChecking=no'
|
||||
|
||||
cmd_str = "ssh #{ssh_opts} #{ssh_usr} '#{host}' "
|
||||
cmd_str << "bash -s <<EOF\n"
|
||||
cmd_str << "export LANG=C\n"
|
||||
cmd_str << "export LC_ALL=C\n"
|
||||
cmd_str << "#{cmd} #{args.join(' ')}\n"
|
||||
cmd_str << 'EOF'
|
||||
|
||||
Open3.capture3(cmd_str)
|
||||
end
|
||||
|
||||
def quote_args(args)
|
||||
args.map do |arg|
|
||||
"'#{arg}'"
|
||||
end
|
||||
end
|
||||
|
||||
end
|
51
src/onevmdump/lib/commons.rb
Normal file
51
src/onevmdump/lib/commons.rb
Normal file
@ -0,0 +1,51 @@
|
||||
# -------------------------------------------------------------------------- #
|
||||
# Copyright 2002-2021, OpenNebula Project, OpenNebula Systems #
|
||||
# #
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may #
|
||||
# not use this file except in compliance with the License. You may obtain #
|
||||
# a copy of the License at #
|
||||
# #
|
||||
# http://www.apache.org/licenses/LICENSE-2.0 #
|
||||
# #
|
||||
# Unless required by applicable law or agreed to in writing, software #
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, #
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. #
|
||||
# See the License for the specific language governing permissions and #
|
||||
# limitations under the License. #
|
||||
#--------------------------------------------------------------------------- #
|
||||
|
||||
# Module containing commnon functions for Exporter and Restorer classes
|
||||
module Commons
|
||||
|
||||
private
|
||||
|
||||
def create_tmp_folder(base_path)
|
||||
prefix = 'onevmdump'
|
||||
|
||||
# Create temporal folder
|
||||
rc = @cmd.run('mktemp', '-d', '-p', base_path, "#{prefix}.XXX")
|
||||
|
||||
unless rc[2].success?
|
||||
raise "Error creating temporal directory: #{rc[1]}"
|
||||
end
|
||||
|
||||
# Return STDOUT
|
||||
rc[0].strip
|
||||
end
|
||||
|
||||
def check_state(vm)
|
||||
state = vm.state_str
|
||||
lcm = vm.lcm_state_str
|
||||
|
||||
msg = "Invalid state: #{state}"
|
||||
raise msg unless self.class::VALID_STATES.include?(state)
|
||||
|
||||
msg = "Invalid LCM state: #{lcm}"
|
||||
raise msg unless self.class::VALID_LCM_STATES.include?(lcm)
|
||||
end
|
||||
|
||||
def running?
|
||||
@vm.lcm_state_str == 'RUNNING' || @vm.lcm_state_str == 'BACKUP'
|
||||
end
|
||||
|
||||
end
|
133
src/onevmdump/lib/exporters/base.rb
Normal file
133
src/onevmdump/lib/exporters/base.rb
Normal file
@ -0,0 +1,133 @@
|
||||
# -------------------------------------------------------------------------- #
|
||||
# Copyright 2002-2021, OpenNebula Project, OpenNebula Systems #
|
||||
# #
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may #
|
||||
# not use this file except in compliance with the License. You may obtain #
|
||||
# a copy of the License at #
|
||||
# #
|
||||
# http://www.apache.org/licenses/LICENSE-2.0 #
|
||||
# #
|
||||
# Unless required by applicable law or agreed to in writing, software #
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, #
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. #
|
||||
# See the License for the specific language governing permissions and #
|
||||
# limitations under the License. #
|
||||
#--------------------------------------------------------------------------- #
|
||||
|
||||
require_relative '../command'
|
||||
require_relative '../commons'
|
||||
|
||||
# Base class with the exporters interface
|
||||
class BaseExporter
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# Default configuration options
|
||||
# --------------------------------------------------------------------------
|
||||
DEFAULT_CONF = {
|
||||
# workaround: using "//" as libvirt needs the path to match exactly
|
||||
:ds_location => '/var/lib/one//datastores',
|
||||
:remote_host => nil,
|
||||
:remote_user => nil,
|
||||
:destination_path => nil,
|
||||
:destination_host => nil,
|
||||
:destination_user => nil
|
||||
}
|
||||
|
||||
VALID_STATES = %w[ACTIVE
|
||||
POWEROFF
|
||||
UNDEPLOYED]
|
||||
|
||||
VALID_LCM_STATES = %w[LCM_INIT
|
||||
RUNNING
|
||||
BACKUP
|
||||
BACKUP_POWEROFF
|
||||
BACKUP_UNDEPLOYED]
|
||||
|
||||
def initialize(vm, config)
|
||||
@vm = vm
|
||||
@config = DEFAULT_CONF.merge(config)
|
||||
|
||||
# Will raise an error if invalid state/lcm_state
|
||||
check_state(@vm)
|
||||
|
||||
# Check if the action needs to be live
|
||||
@live = running?
|
||||
|
||||
# Get System DS ID from last history record
|
||||
begin
|
||||
last_hist_rec = Nokogiri.XML(@vm.get_history_record(-1))
|
||||
@sys_ds_id = Integer(last_hist_rec.xpath('//DS_ID').text)
|
||||
rescue StandardError
|
||||
raise 'Cannot retrieve system DS ID. The last history record' \
|
||||
' might be corrupted or it might not exists.'
|
||||
end
|
||||
|
||||
# Get Command
|
||||
@cmd = Command.new(@config[:remote_user], @config[:remote_host])
|
||||
|
||||
# Build VM folder path
|
||||
@vm_path = "#{@config[:ds_location]}/#{@sys_ds_id}/#{@vm.id}"
|
||||
@tmp_path = create_tmp_folder(@vm_path)
|
||||
end
|
||||
|
||||
def export
|
||||
# Export disks
|
||||
@vm.retrieve_xmlelements('//DISK/DISK_ID').each do |disk_id|
|
||||
disk_id = Integer(disk_id.text)
|
||||
|
||||
if @live
|
||||
export_disk_live(disk_id)
|
||||
else
|
||||
export_disk_cold(disk_id)
|
||||
end
|
||||
end
|
||||
|
||||
# Dump VM xml to include it in the final bundle
|
||||
@cmd.run_redirect_output('echo', "#{@tmp_path}/vm.xml", nil, @vm.to_xml)
|
||||
|
||||
create_bundle
|
||||
ensure
|
||||
cleanup
|
||||
end
|
||||
|
||||
def cleanup
|
||||
@cmd.run('rm', '-rf', @tmp_path)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
include Commons
|
||||
|
||||
def gen_bundle_name
|
||||
return "'#{@config[:destination_path]}'" if @config[:destination_path]
|
||||
|
||||
timestamp = Time.now.strftime('%s')
|
||||
"/tmp/onevmdump-#{@vm.id}-#{timestamp}.tar.gz"
|
||||
end
|
||||
|
||||
def create_bundle
|
||||
bundle_name = gen_bundle_name
|
||||
cmd = "tar -C #{@tmp_path} -czS"
|
||||
|
||||
if @config[:destination_host].nil? || @config[:destination_host].empty?
|
||||
dst = " -f #{bundle_name} ."
|
||||
else
|
||||
destination_user = @config[:destination_user]
|
||||
if !destination_user.nil? && !destination_user.empty?
|
||||
usr = "-l '#{destination_user}'"
|
||||
end
|
||||
|
||||
dst = " - . | ssh '#{@config[:destination_host]}' #{usr}" \
|
||||
" \"cat - > #{bundle_name}\""
|
||||
end
|
||||
|
||||
cmd << dst
|
||||
|
||||
rc = @cmd.run_insecure(cmd)
|
||||
|
||||
raise "Error creating bundle file: #{rc[1]}" unless rc[2].success?
|
||||
|
||||
bundle_name
|
||||
end
|
||||
|
||||
end
|
137
src/onevmdump/lib/exporters/file.rb
Normal file
137
src/onevmdump/lib/exporters/file.rb
Normal file
@ -0,0 +1,137 @@
|
||||
# -------------------------------------------------------------------------- #
|
||||
# Copyright 2002-2021, OpenNebula Project, OpenNebula Systems #
|
||||
# #
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may #
|
||||
# not use this file except in compliance with the License. You may obtain #
|
||||
# a copy of the License at #
|
||||
# #
|
||||
# http://www.apache.org/licenses/LICENSE-2.0 #
|
||||
# #
|
||||
# Unless required by applicable law or agreed to in writing, software #
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, #
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. #
|
||||
# See the License for the specific language governing permissions and #
|
||||
# limitations under the License. #
|
||||
#--------------------------------------------------------------------------- #
|
||||
|
||||
require_relative 'base'
|
||||
|
||||
# OneVMDump module
|
||||
#
|
||||
# Module for exporting VM content into a bundle file
|
||||
module OneVMDump
|
||||
|
||||
# FileExporter class
|
||||
class FileExporter < BaseExporter
|
||||
|
||||
private
|
||||
|
||||
#######################################################################
|
||||
# Export methods used by base
|
||||
#######################################################################
|
||||
|
||||
def export_disk_live(disk_id)
|
||||
type = file_type(disk_path(disk_id))
|
||||
|
||||
case type
|
||||
when :qcow2
|
||||
export_qcow2_live(disk_id)
|
||||
when :raw, :cdrom
|
||||
export_raw_live(disk_id)
|
||||
end
|
||||
end
|
||||
|
||||
def export_disk_cold(disk_id)
|
||||
type = file_type(disk_path(disk_id))
|
||||
|
||||
case type
|
||||
when :qcow2
|
||||
export_qcow2_cold(disk_id)
|
||||
when :raw, :cdrom
|
||||
export_raw_cold(disk_id)
|
||||
end
|
||||
end
|
||||
|
||||
#######################################################################
|
||||
# RAW export methods
|
||||
#######################################################################
|
||||
def export_raw_cold(disk_id)
|
||||
path = disk_path(disk_id)
|
||||
dst_path = "#{@tmp_path}/backup.#{File.basename(path)}"
|
||||
|
||||
@cmd.run('cp', path, dst_path)
|
||||
end
|
||||
|
||||
alias export_raw_live export_raw_cold
|
||||
|
||||
#######################################################################
|
||||
# QCOWw export methods
|
||||
#######################################################################
|
||||
|
||||
def export_qcow2_live(disk_id)
|
||||
path = disk_path(disk_id)
|
||||
|
||||
# blockcopy:
|
||||
# Copy a disk backing image chain to a destination.
|
||||
dst_path = "#{@tmp_path}/backup.#{File.basename(path)}"
|
||||
|
||||
@cmd.run('touch', dst_path) # Create file to set ownership
|
||||
rc = @cmd.run('virsh', '-c', 'qemu:///system', 'blockcopy',
|
||||
"one-#{@vm.id}", '--path', path, '--dest',
|
||||
dst_path, '--wait', '--finish')
|
||||
|
||||
raise "Error exporting '#{path}': #{rc[1]}" unless rc[2].success?
|
||||
end
|
||||
|
||||
def export_qcow2_cold(disk_id)
|
||||
path = disk_path(disk_id)
|
||||
dst_path = "#{@tmp_path}/backup.#{File.basename(path)}"
|
||||
|
||||
rc = @cmd.run('qemu-img', 'convert', '-q', '-O', 'qcow2',
|
||||
path, dst_path)
|
||||
|
||||
raise "Error exporting '#{path}': #{rc[1]}" unless rc[2].success?
|
||||
end
|
||||
|
||||
#######################################################################
|
||||
# Helpers
|
||||
#######################################################################
|
||||
|
||||
# Retruns the file type of the given path (it will follow sym links)
|
||||
#
|
||||
# Supported types:
|
||||
# - :qcow2
|
||||
# - :cdrom
|
||||
# - :raw
|
||||
#
|
||||
def file_type(path)
|
||||
real_path = path
|
||||
real_path = "#{@vm_path}/#{readlink(path)}" if symlink?(path)
|
||||
|
||||
raw_type = @cmd.run('file', real_path)[0].strip
|
||||
|
||||
case raw_type
|
||||
when /^.*QEMU QCOW2 Image.*$/
|
||||
:qcow2
|
||||
when /^.*CD-ROM.*$/
|
||||
:cdrom
|
||||
else
|
||||
:raw
|
||||
end
|
||||
end
|
||||
|
||||
def disk_path(disk_id)
|
||||
"#{@vm_path}/disk.#{disk_id}"
|
||||
end
|
||||
|
||||
def symlink?(path)
|
||||
@cmd.run('test', '-L', path)[2].success?
|
||||
end
|
||||
|
||||
def readlink(path)
|
||||
@cmd.run('readlink', path)[0].strip
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
end
|
112
src/onevmdump/lib/exporters/lv.rb
Normal file
112
src/onevmdump/lib/exporters/lv.rb
Normal file
@ -0,0 +1,112 @@
|
||||
# -------------------------------------------------------------------------- #
|
||||
# Copyright 2002-2021, OpenNebula Project, OpenNebula Systems #
|
||||
# #
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may #
|
||||
# not use this file except in compliance with the License. You may obtain #
|
||||
# a copy of the License at #
|
||||
# #
|
||||
# http://www.apache.org/licenses/LICENSE-2.0 #
|
||||
# #
|
||||
# Unless required by applicable law or agreed to in writing, software #
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, #
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. #
|
||||
# See the License for the specific language governing permissions and #
|
||||
# limitations under the License. #
|
||||
#--------------------------------------------------------------------------- #
|
||||
|
||||
require_relative 'base'
|
||||
|
||||
# OneVMDump module
|
||||
#
|
||||
# Module for exporting VM content into a bundle file
|
||||
module OneVMDump
|
||||
|
||||
# BlockExporer class
|
||||
# It exports the content from LVs into a bundle
|
||||
class LVExporter < BaseExporter
|
||||
|
||||
private
|
||||
|
||||
def export_disk_live(disk_id)
|
||||
# Freeze filesystem
|
||||
rc = @cmd.run('virsh', '-c', 'qemu:///system', 'domfsfreeze',
|
||||
"one-#{@vm.id}")
|
||||
|
||||
unless rc[2].success?
|
||||
raise "Error freezing domain: #{rc[1]}"
|
||||
end
|
||||
|
||||
# Take LV snapshot
|
||||
# TODO: Crete snapshot of proportional size of the disk?
|
||||
begin
|
||||
lv = get_device(disk_id)
|
||||
snap_name = "#{File.basename(lv)}-backup-snap"
|
||||
|
||||
rc = @cmd.run('sudo', 'lvcreate', '-s', '-L', '1G', '-n',
|
||||
snap_name, lv)
|
||||
ensure
|
||||
@cmd.run('virsh', '-c', 'qemu:///system', 'domfsthaw',
|
||||
"one-#{@vm.id}")
|
||||
end
|
||||
|
||||
unless rc[2].success?
|
||||
raise "Error creating snapshot for #{lv}: #{rc[1]}"
|
||||
end
|
||||
|
||||
# Dump content
|
||||
snap_lv = "#{File.dirname(lv)}/#{snap_name}"
|
||||
dst_path = dst_path(disk_id)
|
||||
rc = @cmd.run('dd', "if=#{snap_lv}", "of=#{dst_path}")
|
||||
|
||||
unless rc[2].success?
|
||||
raise "Error writting '#{snap_lv}' content into #{dst_path}:" \
|
||||
" #{rc[1]}"
|
||||
end
|
||||
ensure
|
||||
@cmd.run('sudo', 'lvremove', '-f', snap_lv) if snap_lv
|
||||
end
|
||||
|
||||
def export_disk_cold(disk_id)
|
||||
device = get_device(disk_id)
|
||||
dst_path = dst_path(disk_id)
|
||||
active = check_active(device)
|
||||
|
||||
# Activate LV
|
||||
if !active
|
||||
rc = @cmd.run('lvchange', '-ay', device)
|
||||
|
||||
msg = "Error activating '#{device}': #{rc[1]}"
|
||||
raise msg unless rc[2].success?
|
||||
end
|
||||
|
||||
# Dump content
|
||||
rc = @cmd.run('dd', "if=#{device}", "of=#{dst_path}")
|
||||
|
||||
unless rc[2].success?
|
||||
raise "Error writting '#{device}' content into" \
|
||||
" #{dst_path}: #{rc[1]}"
|
||||
end
|
||||
ensure
|
||||
# Ensure LV is in the same state as before
|
||||
@cmd.run('lvchange', '-an', device) unless active
|
||||
end
|
||||
|
||||
########################################################################
|
||||
# Helpers
|
||||
########################################################################
|
||||
|
||||
def get_device(disk_id)
|
||||
"/dev/vg-one-#{@sys_ds_id}/lv-one-#{@vm.id}-#{disk_id}"
|
||||
end
|
||||
|
||||
def check_active(device)
|
||||
@cmd.run('test', '-e', device)[2].success?
|
||||
end
|
||||
|
||||
def dst_path(disk_id)
|
||||
"#{@tmp_path}/backup.disk.#{disk_id}"
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
end
|
90
src/onevmdump/lib/exporters/rbd.rb
Normal file
90
src/onevmdump/lib/exporters/rbd.rb
Normal file
@ -0,0 +1,90 @@
|
||||
# -------------------------------------------------------------------------- #
|
||||
# Copyright 2002-2021, OpenNebula Project, OpenNebula Systems #
|
||||
# #
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may #
|
||||
# not use this file except in compliance with the License. You may obtain #
|
||||
# a copy of the License at #
|
||||
# #
|
||||
# http://www.apache.org/licenses/LICENSE-2.0 #
|
||||
# #
|
||||
# Unless required by applicable law or agreed to in writing, software #
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, #
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. #
|
||||
# See the License for the specific language governing permissions and #
|
||||
# limitations under the License. #
|
||||
#--------------------------------------------------------------------------- #
|
||||
|
||||
require_relative 'base'
|
||||
|
||||
# OneVMDump module
|
||||
#
|
||||
# Module for exporting VM content into a bundle file
|
||||
module OneVMDump
|
||||
|
||||
# RBDExporter class
|
||||
class RBDExporter < BaseExporter
|
||||
|
||||
private
|
||||
|
||||
def export_disk_live(disk_id)
|
||||
# Freeze filesystem
|
||||
rc = @cmd.run('virsh', '-c', 'qemu:///system', 'domfsfreeze',
|
||||
"one-#{@vm.id}")
|
||||
|
||||
unless rc[2].success?
|
||||
raise "Error freezing domain: #{rc[1]}"
|
||||
end
|
||||
|
||||
export_disk_cold(disk_id)
|
||||
ensure
|
||||
@cmd.run('virsh', '-c', 'qemu:///system', 'domfsthaw',
|
||||
"one-#{@vm.id}")
|
||||
end
|
||||
|
||||
def export_disk_cold(disk_id)
|
||||
path = disk_path(disk_id)
|
||||
|
||||
# Export rbd
|
||||
# Assume rbd version 2
|
||||
cmd = rbd_cmd(disk_id)
|
||||
cmd.append('export', path, "#{@tmp_path}/backup.disk.#{disk_id}")
|
||||
rc = @cmd.run(cmd[0], *cmd[1..-1])
|
||||
|
||||
raise "Error exporting '#{path}': #{rc[1]}" unless rc[2].success?
|
||||
end
|
||||
|
||||
########################################################################
|
||||
# Helpers
|
||||
########################################################################
|
||||
|
||||
def rbd_cmd(disk_id)
|
||||
cmd = ['rbd']
|
||||
disk_xpath = "//DISK[DISK_ID = #{disk_id}]"
|
||||
|
||||
# rubocop:disable Layout/LineLength
|
||||
ceph_user = @vm.retrieve_xmlelements("#{disk_xpath}/CEPH_USER")[0].text rescue nil
|
||||
ceph_key = @vm.retrieve_xmlelements("#{disk_xpath}/CEPH_KEY")[0].text rescue nil
|
||||
ceph_conf = @vm.retrieve_xmlelements("#{disk_xpath}/CEPH_CONF")[0].text rescue nil
|
||||
|
||||
cmd.append('--id', ceph_user) if !ceph_user.nil? && !ceph_user.empty?
|
||||
cmd.append('--keyfile', ceph_key) if !ceph_key.nil? && !ceph_key.empty?
|
||||
cmd.append('--conf', ceph_conf) if !ceph_conf.nil? && !ceph_conf.empty?
|
||||
# rubocop:enable Layout/LineLength
|
||||
|
||||
cmd
|
||||
end
|
||||
|
||||
def disk_path(disk_id)
|
||||
disk_xpath = "//DISK[DISK_ID = #{disk_id}]"
|
||||
source = @vm.retrieve_xmlelements("#{disk_xpath}/SOURCE")[0].text
|
||||
|
||||
if source.nil? || source.empty?
|
||||
raise "Error retrieving source from disk #{disk_id}"
|
||||
end
|
||||
|
||||
"#{source}-#{@vm.id}-#{disk_id}"
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
end
|
284
src/onevmdump/lib/restorers/base.rb
Normal file
284
src/onevmdump/lib/restorers/base.rb
Normal file
@ -0,0 +1,284 @@
|
||||
# -------------------------------------------------------------------------- #
|
||||
# Copyright 2002-2021, OpenNebula Project, OpenNebula Systems #
|
||||
# #
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may #
|
||||
# not use this file except in compliance with the License. You may obtain #
|
||||
# a copy of the License at #
|
||||
# #
|
||||
# http://www.apache.org/licenses/LICENSE-2.0 #
|
||||
# #
|
||||
# Unless required by applicable law or agreed to in writing, software #
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, #
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. #
|
||||
# See the License for the specific language governing permissions and #
|
||||
# limitations under the License. #
|
||||
#--------------------------------------------------------------------------- #
|
||||
|
||||
require 'uri'
|
||||
require 'ffi-rzmq'
|
||||
|
||||
require 'opennebula'
|
||||
|
||||
require_relative '../command'
|
||||
require_relative '../commons'
|
||||
|
||||
# Base class with the restorer interface
|
||||
class BaseRestorer
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# Constants
|
||||
# --------------------------------------------------------------------------
|
||||
|
||||
VALID_STATES = %w[DONE
|
||||
POWEROFF
|
||||
UNDEPLOYED]
|
||||
|
||||
VALID_LCM_STATES = %w[LCM_INIT]
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# Default configuration options
|
||||
# --------------------------------------------------------------------------
|
||||
DEFAULT_CONF = {
|
||||
:tmp_location => '/var/tmp/one',
|
||||
:restore_nics => false
|
||||
}
|
||||
|
||||
def initialize(bundle_path, config)
|
||||
@config = DEFAULT_CONF.merge(config)
|
||||
|
||||
@bundle_path = bundle_path
|
||||
@bundle_name = File.basename(bundle_path)
|
||||
|
||||
@cmd = Command.new(nil, nil)
|
||||
end
|
||||
|
||||
def restore
|
||||
client = OpenNebula::Client.new(nil, @config[:endpoint])
|
||||
tmp_path = decompress_bundle
|
||||
|
||||
vm_xml = OpenNebula::XMLElement.build_xml(
|
||||
File.read("#{tmp_path}/vm.xml"),
|
||||
'VM'
|
||||
)
|
||||
|
||||
vm = OpenNebula::VirtualMachine.new(vm_xml, client)
|
||||
|
||||
@config[:tmpl_name] = "backup-#{vm.id}" unless @config[:tmpl_name]
|
||||
|
||||
new_disks = []
|
||||
# Register each bundle disk as a new image
|
||||
Dir.glob("#{tmp_path}/backup.disk.*") do |disk|
|
||||
disk_id = Integer(disk.split('.')[-1])
|
||||
disk_xpath = "/VM//DISK[DISK_ID = #{disk_id}]"
|
||||
|
||||
ds_id = Integer(vm_xml.xpath("#{disk_xpath}/DATASTORE_ID").text)
|
||||
type = vm_xml.xpath("#{disk_xpath}/IMAGE_TYPE").text
|
||||
|
||||
img_tmpl = ''
|
||||
img_tmpl << "NAME=\"#{@config[:tmpl_name]}-disk-#{disk_id}\"\n"
|
||||
img_tmpl << "TYPE=\"#{type}\"\n"
|
||||
img_tmpl << "PATH=\"#{disk}\"\n"
|
||||
|
||||
img = OpenNebula::Image.new(OpenNebula::Image.build_xml, client)
|
||||
rc = img.allocate(img_tmpl, ds_id)
|
||||
|
||||
img = nil if OpenNebula.is_error?(rc)
|
||||
|
||||
new_disks << {
|
||||
:id => disk_id,
|
||||
:img => img
|
||||
}
|
||||
end
|
||||
|
||||
wait_disks_ready(new_disks, client)
|
||||
|
||||
# Important to use XML from the backup
|
||||
tmpl_info = generate_template(vm_xml, new_disks)
|
||||
|
||||
tmpl = OpenNebula::Template.new(OpenNebula::Template.build_xml, client)
|
||||
rc = tmpl.allocate(tmpl_info)
|
||||
|
||||
if OpenNebula.is_error?(rc)
|
||||
# roll back image creation if one fails
|
||||
new_disks.each do |new_disk|
|
||||
OpenNebula::Image.new_with_id(new_disk[:img_id], client).delete
|
||||
end
|
||||
|
||||
raise "Error creating VM Template from backup: #{rc.message}"
|
||||
end
|
||||
|
||||
tmpl.id
|
||||
ensure
|
||||
# cleanup
|
||||
@cmd.run('rm', '-rf', tmp_path) if !tmp_path.nil? && !tmp_path.empty?
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
include Commons
|
||||
|
||||
def decompress_bundle
|
||||
tmp_path = create_tmp_folder(@config[:tmp_location])
|
||||
|
||||
rc = @cmd.run('tar', '-C', tmp_path, '-xf', @bundle_path)
|
||||
|
||||
raise "Error decompressing bundle file: #{rc[1]}" unless rc[2].success?
|
||||
|
||||
tmp_path
|
||||
end
|
||||
|
||||
def generate_template(vm_xml, new_disks)
|
||||
template = ''
|
||||
|
||||
begin
|
||||
template << "NAME = \"#{@config[:tmpl_name]}\"\n"
|
||||
template << "CPU = \"#{vm_xml.xpath('TEMPLATE/CPU').text}\"\n"
|
||||
template << "VCPU = \"#{vm_xml.xpath('TEMPLATE/VCPU').text}\"\n"
|
||||
template << "MEMORY = \"#{vm_xml.xpath('TEMPLATE/MEMORY').text}\"\n"
|
||||
template << "DESCRIPTION = \"VM restored from backup.\"\n"
|
||||
|
||||
# Add disks
|
||||
disk_black_list = Set.new(%w[ALLOW_ORPHANS CLONE CLONE_TARGET
|
||||
CLUSTER_ID DATASTORE DATASTORE_ID
|
||||
DEV_PREFIX DISK_ID
|
||||
DISK_SNAPSHOT_TOTAL_SIZE DISK_TYPE
|
||||
DRIVER IMAGE IMAGE_ID IMAGE_STATE
|
||||
IMAGE_UID IMAGE_UNAME LN_TARGET
|
||||
OPENNEBULA_MANAGED ORIGINAL_SIZE
|
||||
PERSISTENT READONLY SAVE SIZE SOURCE
|
||||
TARGET TM_MAD TYPE])
|
||||
new_disks.each do |disk|
|
||||
disk_xpath = "/VM//DISK[DISK_ID = #{disk[:id]}]/*"
|
||||
disk_tmpl = "DISK = [\n"
|
||||
|
||||
vm_xml.xpath(disk_xpath).each do |item|
|
||||
# Add every attribute but image related ones
|
||||
next if disk_black_list.include?(item.name)
|
||||
|
||||
disk_tmpl << "#{item.name} = \"#{item.text}\",\n"
|
||||
end
|
||||
|
||||
disk_tmpl << "IMAGE_ID = #{disk[:img].id}]\n"
|
||||
|
||||
template << disk_tmpl
|
||||
end
|
||||
|
||||
# Add NICs
|
||||
nic_black_list = Set.new(%w[AR_ID BRIDGE BRIDGE_TYPE CLUSTER_ID IP
|
||||
IP6 IP6_ULA IP6_GLOBAL NAME NETWORK_ID
|
||||
NIC_ID TARGET VLAN_ID VN_MAD])
|
||||
if @config[:restore_nics]
|
||||
%w[NIC NIC_ALIAS].each do |type|
|
||||
vm_xml.xpath("/VM//#{type}").each do |nic|
|
||||
nic_tmpl = "#{type} = [\n"
|
||||
|
||||
nic.xpath('./*').each do |item|
|
||||
next if nic_black_list.include?(item.name)
|
||||
|
||||
nic_tmpl << "#{item.name} = \"#{item.text}\",\n"
|
||||
end
|
||||
|
||||
# remove ',\n' for last elem
|
||||
template << nic_tmpl[0..-3] << "]\n"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
###########################################################
|
||||
# TODO, evaluate what else should be copy from original VM
|
||||
###########################################################
|
||||
rescue StandardError => e
|
||||
msg = 'Error parsing VM information. '
|
||||
msg << "#{e.message}\n#{e.backtrace}" if @config[:debug]
|
||||
|
||||
raise msg
|
||||
end
|
||||
|
||||
template
|
||||
end
|
||||
|
||||
def wait_disks_ready(disks, client)
|
||||
context = ZMQ::Context.new(1)
|
||||
subscriber = context.socket(ZMQ::SUB)
|
||||
|
||||
poller = ZMQ::Poller.new
|
||||
poller.register(subscriber, ZMQ::POLLIN)
|
||||
|
||||
uri = URI(@config[:endpoint])
|
||||
error = false
|
||||
|
||||
subscriber.connect("tcp://#{uri.host}:2101")
|
||||
|
||||
# Subscribe for every IMAGE
|
||||
imgs_set = Set.new
|
||||
disks.each do |disk|
|
||||
if disk[:img].nil?
|
||||
error = true
|
||||
next
|
||||
end
|
||||
|
||||
# subscribe to wait until every image is ready
|
||||
img_id = disk[:img].id
|
||||
|
||||
%w[READY ERROR].each do |i|
|
||||
key = "EVENT IMAGE #{img_id}/#{i}/"
|
||||
subscriber.setsockopt(ZMQ::SUBSCRIBE, key)
|
||||
end
|
||||
|
||||
imgs_set.add(Integer(img_id))
|
||||
end
|
||||
|
||||
# Wait until every image is ready (or limit retries)
|
||||
retries = 60
|
||||
key = ''
|
||||
content = ''
|
||||
while !imgs_set.empty? && retries > 0
|
||||
if retries % 10 == 0
|
||||
# Check manually in case the event is missed
|
||||
imgs_set.clone.each do |id|
|
||||
img = OpenNebula::Image.new_with_id(id, client)
|
||||
img.info
|
||||
|
||||
next unless %w[READY ERROR].include?(img.state_str)
|
||||
|
||||
error = true if img.state_str.upcase == 'ERROR'
|
||||
imgs_set.delete(id)
|
||||
end
|
||||
end
|
||||
|
||||
break if imgs_set.empty?
|
||||
|
||||
# 60 retries * 60 secs timeout (select) 1h timeout worst case
|
||||
if !poller.poll(60 * 1000).zero?
|
||||
subscriber.recv_string(key)
|
||||
subscriber.recv_string(content)
|
||||
|
||||
match = key.match(%r{EVENT IMAGE (?<img_id>\d+)/(?<state>\S+)/})
|
||||
img_id = Integer(match[:img_id])
|
||||
|
||||
%w[READY ERROR].each do |i|
|
||||
key = "EVENT IMAGE #{img_id}/#{i}/"
|
||||
subscriber.setsockopt(ZMQ::UNSUBSCRIBE, key)
|
||||
end
|
||||
|
||||
error = true if match[:state] == 'ERROR'
|
||||
imgs_set.delete(img_id)
|
||||
else
|
||||
retries -= 1
|
||||
end
|
||||
end
|
||||
|
||||
raise 'Error allocating new images.' if error
|
||||
ensure
|
||||
# Close socket
|
||||
subscriber.close
|
||||
|
||||
# Rollback - remove every image if error
|
||||
if error
|
||||
disks.each do |disk|
|
||||
disk[:img].delete unless disk[:img].nil?
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
end
|
261
src/onevmdump/onevmdump
Executable file
261
src/onevmdump/onevmdump
Executable file
@ -0,0 +1,261 @@
|
||||
#!/usr/bin/ruby
|
||||
|
||||
# -------------------------------------------------------------------------- #
|
||||
# Copyright 2002-2021, OpenNebula Project, OpenNebula Systems #
|
||||
# #
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may #
|
||||
# not use this file except in compliance with the License. You may obtain #
|
||||
# a copy of the License at #
|
||||
# #
|
||||
# http://www.apache.org/licenses/LICENSE-2.0 #
|
||||
# #
|
||||
# Unless required by applicable law or agreed to in writing, software #
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, #
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. #
|
||||
# See the License for the specific language governing permissions and #
|
||||
# limitations under the License. #
|
||||
#--------------------------------------------------------------------------- #
|
||||
|
||||
############################################################################
|
||||
# Set up Frontend libraries location
|
||||
############################################################################
|
||||
|
||||
ONE_LOCATION = ENV['ONE_LOCATION']
|
||||
|
||||
if !ONE_LOCATION
|
||||
RUBY_LIB_LOCATION = '/usr/lib/one/ruby'
|
||||
GEMS_LOCATION = '/usr/share/one/gems'
|
||||
VMDIR = '/var/lib/one'
|
||||
CONFIG_FILE = '/var/lib/one/config'
|
||||
LOG_FILE = '/var/log/one/host_error.log'
|
||||
else
|
||||
RUBY_LIB_LOCATION = ONE_LOCATION + '/lib/ruby'
|
||||
GEMS_LOCATION = ONE_LOCATION + '/share/gems'
|
||||
VMDIR = ONE_LOCATION + '/var'
|
||||
CONFIG_FILE = ONE_LOCATION + '/var/config'
|
||||
LOG_FILE = ONE_LOCATION + '/var/host_error.log'
|
||||
end
|
||||
|
||||
# %%RUBYGEMS_SETUP_BEGIN%%
|
||||
if File.directory?(GEMS_LOCATION)
|
||||
real_gems_path = File.realpath(GEMS_LOCATION)
|
||||
if !defined?(Gem) || Gem.path != [real_gems_path]
|
||||
$LOAD_PATH.reject! {|l| l =~ /vendor_ruby/ }
|
||||
|
||||
# Suppress warnings from Rubygems
|
||||
# https://github.com/OpenNebula/one/issues/5379
|
||||
begin
|
||||
verb = $VERBOSE
|
||||
$VERBOSE = nil
|
||||
require 'rubygems'
|
||||
Gem.use_paths(real_gems_path)
|
||||
ensure
|
||||
$VERBOSE = verb
|
||||
end
|
||||
end
|
||||
end
|
||||
# %%RUBYGEMS_SETUP_END%%
|
||||
|
||||
$LOAD_PATH << RUBY_LIB_LOCATION
|
||||
$LOAD_PATH << RUBY_LIB_LOCATION + '/onevmdump'
|
||||
|
||||
############################################################################
|
||||
# Required libraries
|
||||
############################################################################
|
||||
|
||||
require 'nokogiri'
|
||||
require 'optparse'
|
||||
require 'opennebula'
|
||||
|
||||
require 'onevmdump'
|
||||
|
||||
############################################################################
|
||||
# Constants
|
||||
############################################################################
|
||||
|
||||
HELP_MSG = <<~EOF
|
||||
COMMANDS
|
||||
\texport\t\tExports VM into a bundle file.
|
||||
\trestore\t\tRestore a VM from a bundle file.
|
||||
|
||||
\tCheck 'onevmdump COMMAND --help' for more information on an specific command.
|
||||
EOF
|
||||
|
||||
############################################################################
|
||||
# Parameters initalization
|
||||
############################################################################
|
||||
|
||||
options = {
|
||||
:endpoint => 'http://localhost:2633/RPC2'
|
||||
}
|
||||
|
||||
############################################################################
|
||||
# General optinos parser
|
||||
############################################################################
|
||||
|
||||
global_parser = OptionParser.new do |opts|
|
||||
opts.banner = 'Usage: onevmdump [options] [COMMAND [options]]'
|
||||
|
||||
desc = 'Run it on debug mode'
|
||||
opts.on('-D', '--debug', desc) do |v|
|
||||
options[:debug] = v
|
||||
end
|
||||
|
||||
desc = 'OpenNebula endpoint (default http://localhost:2633/RPC2)'
|
||||
opts.on('--endpoint=ENDPOINT', desc) do |v|
|
||||
options[:endpoint] = v
|
||||
end
|
||||
|
||||
opts.separator ''
|
||||
opts.separator HELP_MSG
|
||||
end
|
||||
|
||||
############################################################################
|
||||
# Commands optinos parser
|
||||
############################################################################
|
||||
|
||||
commands_parsers = {
|
||||
'export' => OptionParser.new do |opts|
|
||||
opts.banner = 'Usage: onevmdump export [options] <vm_id>'
|
||||
options[:lock] = true
|
||||
|
||||
desc = 'Path where the bundle will be created (default /tmp)'
|
||||
opts.on('-dPATH', '--destination-path=PATH', desc) do |v|
|
||||
options[:destination_path] = v
|
||||
end
|
||||
|
||||
desc = 'Destination host for the bundle'
|
||||
opts.on('--destination-host=HOST', desc) do |v|
|
||||
options[:destination_host] = v
|
||||
end
|
||||
|
||||
desc = 'Avoid locking the VM while doing the backup (another security' \
|
||||
' messures should be taken to avoid un expected status changes.)'
|
||||
opts.on('-L', '--no-lock', desc) do |v|
|
||||
options[:lock] = v # as parameter is *no*-lock if set v == false
|
||||
end
|
||||
|
||||
desc = 'Remote user for accessing destination host via SSH'
|
||||
opts.on('--destination-user=USER', desc) do |v|
|
||||
options[:destination_user] = v
|
||||
end
|
||||
|
||||
desc = 'Remote host, used when the VM storage is not available from ' \
|
||||
'the curren node'
|
||||
opts.on('-hHOST', '--remote-host=HOST', desc) do |v|
|
||||
options[:remote_host] = v
|
||||
end
|
||||
|
||||
desc = 'Remote user for accessing remote host via SSH'
|
||||
opts.on('-lUSER', '--remote-user=USER', desc) do |v|
|
||||
options[:remote_user] = v
|
||||
end
|
||||
|
||||
desc = 'Instead of retrieving the VM XML querying the endpoint, it ' \
|
||||
'will be readed from STDIN. The VM id will be automatically ' \
|
||||
'retrieved from the XML'
|
||||
opts.on(nil, '--stdin', desc) do |v|
|
||||
options[:stdin] = v
|
||||
end
|
||||
end,
|
||||
'restore' => OptionParser.new do |opts|
|
||||
opts.banner = 'Usage: onevmdump restore [options] <backup_file>'
|
||||
|
||||
desc = 'Instantiate backup resulting template automatically'
|
||||
opts.on('--instantiate', desc) do |v|
|
||||
options[:instantiate] = v
|
||||
end
|
||||
|
||||
desc = 'Name for the resulting VM Template'
|
||||
opts.on('-nNAME', '--name=NAME', desc) do |v|
|
||||
options[:tmpl_name] = v
|
||||
end
|
||||
|
||||
desc = 'Force restore of original VM NICs'
|
||||
opts.on('--restore-nics', desc) do |v|
|
||||
options[:restore_nics] = v
|
||||
end
|
||||
end
|
||||
}
|
||||
|
||||
############################################################################
|
||||
# Options parsing
|
||||
############################################################################
|
||||
|
||||
begin
|
||||
global_parser.order!
|
||||
command = ARGV.shift
|
||||
|
||||
raise 'A valid comman must be provided' if command.nil? || command.empty?
|
||||
raise "Invalid command: #{command}" if commands_parsers[command].nil?
|
||||
|
||||
commands_parsers[command].parse!
|
||||
rescue StandardError => e
|
||||
STDERR.puts "ERROR parsing commands: #{e.message}"
|
||||
exit(-1)
|
||||
end
|
||||
|
||||
############################################################################
|
||||
# Main Program
|
||||
#
|
||||
# TODO
|
||||
# - Multithreading for multiple disks (async/await?)
|
||||
# - Add incremental
|
||||
############################################################################
|
||||
|
||||
begin
|
||||
client = OpenNebula::Client.new(nil, options[:endpoint])
|
||||
|
||||
case command
|
||||
when 'export'
|
||||
if options[:stdin]
|
||||
vm_xml = OpenNebula::XMLElement.build_xml(STDIN.read, 'VM')
|
||||
vm = OpenNebula::VirtualMachine.new(vm_xml, client)
|
||||
vm.lock(4) if options[:lock]
|
||||
else
|
||||
begin
|
||||
vm_id = Integer(ARGV[0])
|
||||
rescue ArgumentError, TypeError
|
||||
raise 'A VM ID must be provided.'
|
||||
end
|
||||
|
||||
vm = OpenNebula::VirtualMachine.new_with_id(vm_id, client)
|
||||
vm.lock(4) if options[:lock]
|
||||
rc = vm.info
|
||||
|
||||
if OpenNebula.is_error?(rc)
|
||||
raise "Error getting VM info: #{rc.message}"
|
||||
end
|
||||
end
|
||||
|
||||
# Export VM. VM folder will be used as temporal storage
|
||||
exporter = OneVMDump.get_exporter(vm, options)
|
||||
bundle_location = exporter.export
|
||||
|
||||
puts bundle_location
|
||||
when 'restore'
|
||||
raise 'Bundle path must be provided' if ARGV[0].nil? || ARGV[0].empty?
|
||||
|
||||
tmpl = OneVMDump.get_restorer(ARGV[0], options).restore
|
||||
|
||||
if options[:instantiate]
|
||||
rc = OpenNebula::Template.new_with_id(tmpl, client).instantiate
|
||||
|
||||
if OpenNebula.is_error?(rc)
|
||||
raise "Error instantiating VM template: #{rc.message}"
|
||||
end
|
||||
|
||||
puts "VM Restored: #{rc}"
|
||||
else
|
||||
puts "VM Template restored: #{tmpl}"
|
||||
end
|
||||
end
|
||||
rescue StandardError => e
|
||||
STDERR.puts e.message
|
||||
STDERR.puts e.backtrace if options[:debug]
|
||||
exit(-1)
|
||||
ensure
|
||||
vm.unlock if options[:lock] && !vm.nil?
|
||||
end
|
||||
|
||||
exit(0)
|
56
src/onevmdump/onevmdump.rb
Normal file
56
src/onevmdump/onevmdump.rb
Normal file
@ -0,0 +1,56 @@
|
||||
# -------------------------------------------------------------------------- #
|
||||
# Copyright 2002-2021, OpenNebula Project, OpenNebula Systems #
|
||||
# #
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may #
|
||||
# not use this file except in compliance with the License. You may obtain #
|
||||
# a copy of the License at #
|
||||
# #
|
||||
# http://www.apache.org/licenses/LICENSE-2.0 #
|
||||
# #
|
||||
# Unless required by applicable law or agreed to in writing, software #
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, #
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. #
|
||||
# See the License for the specific language governing permissions and #
|
||||
# limitations under the License. #
|
||||
#--------------------------------------------------------------------------- #
|
||||
|
||||
require 'nokogiri'
|
||||
|
||||
require 'lib/exporters/file'
|
||||
require 'lib/exporters/rbd'
|
||||
require 'lib/exporters/lv'
|
||||
|
||||
require 'lib/restorers/base'
|
||||
|
||||
# OneVMDump module
|
||||
#
|
||||
# Module for exporting VM content into a bundle file
|
||||
module OneVMDump
|
||||
|
||||
def self.get_exporter(vm, config)
|
||||
# Get TM_MAD from last history record
|
||||
begin
|
||||
last_hist_rec = Nokogiri.XML(vm.get_history_record(-1))
|
||||
tm_mad = last_hist_rec.xpath('//TM_MAD').text
|
||||
rescue StandardError
|
||||
raise 'Cannot retrieve TM_MAD. The last history record' \
|
||||
' might be corrupted or it might not exists.'
|
||||
end
|
||||
|
||||
case tm_mad
|
||||
when 'ceph'
|
||||
self::RBDExporter.new(vm, config)
|
||||
when 'ssh', 'shared', 'qcow2'
|
||||
self::FileExporter.new(vm, config)
|
||||
when 'fs_lvm', 'fs_lvm_ssh'
|
||||
self::LVExporter.new(vm, config)
|
||||
else
|
||||
raise "Unsopported TM_MAD: '#{tm_mad}'"
|
||||
end
|
||||
end
|
||||
|
||||
def self.get_restorer(bundle_path, options)
|
||||
BaseRestorer.new(bundle_path, options)
|
||||
end
|
||||
|
||||
end
|
Loading…
x
Reference in New Issue
Block a user