diff --git a/share/doc/xsd/vm.xsd b/share/doc/xsd/vm.xsd
index e4040c73d2..12a231005b 100644
--- a/share/doc/xsd/vm.xsd
+++ b/share/doc/xsd/vm.xsd
@@ -305,6 +305,7 @@
+
diff --git a/src/cli/onevm b/src/cli/onevm
index 1e4e36b094..3710e49cf2 100755
--- a/src/cli/onevm
+++ b/src/cli/onevm
@@ -1343,7 +1343,7 @@ CommandParser::CmdParser.new(ARGV) do
VIDEO = ["TYPE", "IOMMU", "ATS", "VRAM", "RESOLUTION"]
RAW = ["DATA", "DATA_VMX", "TYPE", "VALIDATE"]
CPU_MODEL = ["MODEL"]
- BACKUP_CONFIG = ["FS_FREEZE", "KEEP_LAST", "BACKUP_VOLATILE", "MODE"]
+ BACKUP_CONFIG = ["FS_FREEZE", "KEEP_LAST", "BACKUP_VOLATILE", "MODE", "INCREMENT_MODE"]
CONTEXT (any value, **variable substitution will be made**)
EOT
diff --git a/src/tm_mad/lib/backup_qcow2.rb b/src/tm_mad/lib/backup_qcow2.rb
index f78ef6ebed..2fde46c0a0 100755
--- a/src/tm_mad/lib/backup_qcow2.rb
+++ b/src/tm_mad/lib/backup_qcow2.rb
@@ -21,6 +21,7 @@ require 'open3'
require 'rexml/document'
require 'base64'
require 'getoptlong'
+require 'fileutils'
require_relative 'kvm'
@@ -206,22 +207,67 @@ end
# ------------------------------------------------------------------------------
# This class abstracts the information and several methods to operate over
# disk images files
-# ------------------------------------------------------------------------------
+#
+# +--------+---------------+---------------------------------------------------+
+# | Driver | PType | Layout |
+# +========+===============+===================================================+
+# | NFS | persistent | disk.0 -> /var/lib/one/datastores/1/ab24b7.snap/0 |
+# | | no persistent | disk.0 -> disk.0.snap/0 |
+# +----------------------------------------------------------------------------+
+# | SSH | persistent | disk.0 |
+# | | no persistent | (no link regular file) |
+# +--------+---------------+---------------------------------------------------+
+#
+# NOTE: A persistent SSH disk may be a link to a pevious snapshot
+#
+# @name of the file supporting the disk
+# @snap path for the snapshot folder (absolute)
+# @path real path (links resolved of the file)
+# @slink path to symlink disk.i file (= @snap for persistent)
class QemuImg
include Command
- def initialize(path)
- @path = path
+ attr_reader :path, :name, :snap, :slink
+
+ def initialize(path, opts = {})
@_info = nil
- @path = File.realpath(path) if File.exist?(path)
+ read_paths(path, opts)
+ end
+
+ def read_paths(path, opts = {})
+ # Default locations
+ @path = path
+ @name = File.basename(path)
+ @snap = "#{opts[:vm_dir]}/disk.#{opts[:disk_id]}.snap"
+
+ return if opts.empty? || !File.exist?(path)
+
+ @path = File.realpath(path)
+
+ rl = begin
+ File.readlink(path)
+ rescue StandardError
+ nil
+ end
+
+ return unless rl
+
+ @name = File.basename(rl)
+ @slink = File.dirname(rl)
+
+ @snap = if opts[:shared] && opts[:persistent]
+ @slink
+ else
+ File.dirname("#{opts[:vm_dir]}/#{rl}")
+ end
end
#---------------------------------------------------------------------------
# qemu-img command methods
#---------------------------------------------------------------------------
- QEMU_IMG_COMMANDS = ['convert', 'create', 'rebase', 'info', 'bitmap']
+ QEMU_IMG_COMMANDS = ['convert', 'create', 'rebase', 'info', 'bitmap', 'commit']
QEMU_IMG_COMMANDS.each do |command|
define_method(command.to_sym) do |args = '', opts|
@@ -403,7 +449,7 @@ class KVMDomain
include TransferManager::KVM
include Command
- attr_reader :parent_id, :backup_id, :checkpoint, :tmp_dir, :bck_dir
+ attr_reader :parent_id, :backup_id, :checkpoint, :tmp_dir, :bck_dir, :inc_mode
#---------------------------------------------------------------------------
# @param vm[REXML::Document] OpenNebula XML VM information
@@ -419,8 +465,6 @@ class KVMDomain
@backup_id = 0
@parent_id = -1
- @checkpoint = false
-
mode = @vm.elements['BACKUPS/BACKUP_CONFIG/MODE']
if mode
@@ -429,14 +473,19 @@ class KVMDomain
@backup_id = 0
@parent_id = -1
- @checkpoint = false
+ @inc_mode = :none
when 'INCREMENT'
li = @vm.elements['BACKUPS/BACKUP_CONFIG/LAST_INCREMENT_ID'].text.to_i
+ im = @vm.elements['BACKUPS/BACKUP_CONFIG/INCREMENT_MODE']
+
+ if im && 'snapshot'.casecmp(im.text) == 0
+ @inc_mode = :snapshot
+ else
+ @inc_mode = :cbt
+ end
@backup_id = li + 1
@parent_id = li
-
- @checkpoint = true
end
end
@@ -595,7 +644,7 @@ class KVMDomain
# the backup operation in case it fails
#---------------------------------------------------------------------------
def clean_checkpoints(disks_s, all = false)
- return unless @checkpoint
+ return if @inc_mode != :cbt
# Get a list of dirty checkpoints
check_ids = checkpoints
@@ -640,7 +689,7 @@ class KVMDomain
fsfreeze
- start_backup(disks, @backup_id, @parent_id, @checkpoint)
+ start_backup(disks, @backup_id, @parent_id, @inc_mode == :cbt)
fsthaw
@@ -668,6 +717,77 @@ class KVMDomain
stop_backup
end
+ def backup_snapshot_live(disks_s)
+ init = Time.now
+ disks = disks_s.split ':'
+ dspec = []
+ qdisks = []
+
+ @vm.elements.each 'TEMPLATE/DISK' do |d|
+ did = d.elements['DISK_ID'].text
+ tgt = d.elements['TARGET'].text
+ per = d.elements['SAVE'].text.casecmp('YES') == 0
+ ssh = d.elements['TM_MAD'].text.casecmp('SSH') == 0
+
+ next unless disks.include? did
+
+ disk = QemuImg.new("#{@vm_dir}/disk.#{did}",
+ :vm_dir => @vm_dir,
+ :disk_id => did,
+ :persistent => per,
+ :shared => !ssh)
+
+ qdisks << { :did => did, :tgt => tgt, :disk => disk }
+
+ bfile = disk['full-backing-filename']
+
+ next_path = "#{disk.snap}/#{@backup_id.to_i + 1}"
+ next_disk = QemuImg.new(next_path)
+
+ next_disk.create(:f => 'qcow2', :o => 'backing_fmt=qcow2', :b => bfile)
+
+ dspec << "#{tgt},file=#{next_path}"
+ end
+
+ opts = {
+ :disk_only => '',
+ :atomic => '',
+ :reuse_external => '',
+ :no_metadata => '',
+ :diskspec => dspec
+ }
+
+ fsfreeze
+
+ cmd("#{virsh} snapshot-create-as", @dom, opts)
+
+ fsthaw
+
+ qdisks.each do |qdisk|
+ did = qdisk[:did]
+ tgt = qdisk[:tgt]
+ disk = qdisk[:disk]
+
+ back_path = "#{@bck_dir}/disk.#{did}.#{@backup_id}"
+
+ FileUtils.cp(disk.path, back_path)
+
+ back_img = QemuImg.new back_path
+ back_img.rebase(:U => '', :u => '', :F => 'qcow2', :q => '', :b => '""')
+
+ cmd("#{virsh} blockcommit", "#{@dom} #{tgt}",
+ :wait => '',
+ :top => disk.path,
+ :base => "#{disk.snap}/0")
+
+ FileUtils.rm(disk.path, :force => true)
+
+ symlink("#{disk.slink}/#{@backup_id.to_i + 1}", "#{@vm_dir}/disk.#{did}")
+ end
+
+ log("[BCK]: Incremental backup done in #{Time.now - init}s")
+ end
+
def backup_full_live(disks_s)
init = Time.now
disks = disks_s.split ':'
@@ -679,18 +799,54 @@ class KVMDomain
@vm.elements.each 'TEMPLATE/DISK' do |d|
did = d.elements['DISK_ID'].text
tgt = d.elements['TARGET'].text
+ per = d.elements['SAVE'].text.casecmp('YES') == 0
+ ssh = d.elements['TM_MAD'].text.casecmp('SSH') == 0
next unless disks.include? did
- overlay = "#{@tmp_dir}/overlay_#{did}.qcow2"
+ disk_path = "#{@vm_dir}/disk.#{did}"
+ disk_opts = {
+ :vm_dir => @vm_dir,
+ :disk_id => did,
+ :persistent => per,
+ :shared => !ssh
+ }
- File.open(overlay, 'w') {}
+ qdisk[did] = QemuImg.new(disk_path, disk_opts)
+
+ if @inc_mode == :snapshot
+ base = "#{qdisk[did].slink}/0"
+
+ if snap_folder(did, true)
+ qdisk[did].read_paths(disk_path, disk_opts)
+ elsif qdisk[did].name != '0'
+ cmd("#{virsh} blockcommit", "#{@dom} #{tgt}",
+ :active => '',
+ :pivot => '',
+ :wait => '',
+ :top => qdisk[did].path,
+ :base => base)
+
+ FileUtils.rm(qdisk[did].path, :force => true)
+
+ symlink(base, "#{@vm_dir}/disk.#{did}")
+
+ qdisk[did].read_paths(disk_path, disk_opts)
+ end
+
+ overlay = "#{qdisk[did].snap}/1"
+ odisk = QemuImg.new(overlay)
+
+ odisk.create(:f => 'qcow2', :o => 'backing_fmt=qcow2', :b => base)
+ else
+ overlay = "#{@tmp_dir}/overlay_#{did}.qcow2"
+
+ File.open(overlay, 'w') {}
+ end
dspec << "#{tgt},file=#{overlay}"
disk_xml << ""
-
- qdisk[did] = QemuImg.new("#{@vm_dir}/disk.#{did}")
end
disk_xml << ''
@@ -702,6 +858,8 @@ class KVMDomain
:diskspec => dspec
}
+ opts.merge!({ :no_metadata =>'', :reuse_external => '' }) if @inc_mode == :snapshot
+
checkpoint_xml = <<~EOS
one-#{@vid}-0
@@ -717,12 +875,18 @@ class KVMDomain
cmd("#{virsh} snapshot-create-as", @dom, opts)
- cmd("#{virsh} checkpoint-create", @dom, :xmlfile => cpath) if @checkpoint
+ if @inc_mode == :cbt
+ cmd("#{virsh} checkpoint-create", @dom, :xmlfile => cpath)
+ end
fsthaw
qdisk.each do |did, disk|
disk.convert("#{@bck_dir}/disk.#{did}.0", :m => '4', :O => 'qcow2', :U => '')
+
+ next unless @inc_mode == :snapshot
+
+ symlink("#{disk.slink}/1", "#{@vm_dir}/disk.#{did}")
end
log("[BCK]: Full backup done in #{Time.now - init}s")
@@ -756,7 +920,62 @@ class KVMDomain
#---------------------------------------------------------------------------
# Make a backup for the VM. (see make_backup_live)
+ #
+ # First Backup Nth Backup
+ # @parent_id = -1 @parent_id = n
+ # @backup_id = 0 @backup_id = n+1
+ # snapshot = 1 snapshot = n+2
+ #
+ # ├── backup ├── backup
+ # │ ├── disk.0.0 │ ├── disk.0.n+1
+ # │ └── vm.xml │ └── vm.xml
+ # ├── disk.0 -> disk.0.snap/1 ├── disk.0 -> disk.0.snap/n+2
+ # └── disk.0.snap └── disk.0.snap
+ # ├── 0 (backingfile --> image) ├── 0 (backingfile --> image)
+ # ├── 1 (backingfile --> 0) ├── n+1 (commit to 0)
+ # └── disk.0.snap -> . ├── n+2 (backingfile --> 0)
+ # └── disk.0.snap -> .
#---------------------------------------------------------------------------
+ def backup_snapshot(disks_s)
+ init = Time.now
+ disks = disks_s.split ':'
+
+ @vm.elements.each 'TEMPLATE/DISK' do |d|
+ did = d.elements['DISK_ID'].text
+ per = d.elements['SAVE'].text.casecmp('YES') == 0
+ ssh = d.elements['TM_MAD'].text.casecmp('SSH') == 0
+
+ next unless disks.include? did
+
+ disk_path = "#{@vm_dir}/disk.#{did}"
+ disk_opts = {
+ :vm_dir => @vm_dir,
+ :disk_id => did,
+ :persistent => per,
+ :shared => !ssh
+ }
+
+ disk = QemuImg.new(disk_path, disk_opts)
+
+ FileUtils.cp(disk.path, "#{@bck_dir}/disk.#{did}.#{@backup_id}")
+
+ back_disk = QemuImg.new("#{@bck_dir}/disk.#{did}.#{@backup_id}")
+ back_disk.rebase(:U => '', :u => '', :F => 'qcow2', :q => '', :b => '""')
+
+ bfile = disk['full-backing-filename']
+ disk.commit(:q => '')
+
+ next_disk = QemuImg.new("#{disk.snap}/#{@backup_id.to_i + 1}")
+ next_disk.create(:f => 'qcow2', :o => 'backing_fmt=qcow2', :b => bfile)
+
+ FileUtils.rm(disk.path, :force => true)
+
+ symlink("#{disk.slink}/#{@backup_id.to_i + 1}", disk_path)
+ end
+
+ log("[BCK]: Snapshot incremental backup done in #{Time.now - init}s")
+ end
+
def backup_nbd(disks_s)
init = Time.now
disks = disks_s.split ':'
@@ -787,6 +1006,7 @@ class KVMDomain
Nbd.stop_nbd
end
+ # rubocop:disable Style/CombinableLoops
dids.each do |d|
idisk = QemuImg.new("#{@vm_dir}/disk.#{d}")
@@ -797,9 +1017,10 @@ class KVMDomain
end
idisk.bitmap("one-#{@vid}-#{@backup_id}", :add => '')
- end if @checkpoint
+ end
+ # rubocop:enable Style/CombinableLoops
- log("[BCK]: Incremental backup done in #{Time.now - init}s")
+ log("[BCK]: CBT incremental backup done in #{Time.now - init}s")
end
def backup_full(disks_s)
@@ -808,20 +1029,50 @@ class KVMDomain
@vm.elements.each 'TEMPLATE/DISK' do |d|
did = d.elements['DISK_ID'].text
+ per = d.elements['SAVE'].text.casecmp('YES') == 0
+ ssh = d.elements['TM_MAD'].text.casecmp('SSH') == 0
next unless disks.include? did
- sdisk = QemuImg.new("#{@vm_dir}/disk.#{did}")
- ddisk = "#{@bck_dir}/disk.#{did}.0"
+ disk_path = "#{@vm_dir}/disk.#{did}"
+ disk_opts = {
+ :vm_dir => @vm_dir,
+ :disk_id => did,
+ :persistent => per,
+ :shared => !ssh
+ }
- sdisk.convert(ddisk, :m => '4', :O => 'qcow2', :U => '')
+ sdisk = QemuImg.new(disk_path, disk_opts)
- next unless @checkpoint
+ sdisk.convert("#{@bck_dir}/disk.#{did}.0", :m => '4', :O => 'qcow2', :U => '')
- bms = sdisk.bitmaps
- bms.each {|bm| sdisk.bitmap(bm['name'], :remove => '') } unless bms.nil?
+ case @inc_mode
+ when :cbt
+ bms = sdisk.bitmaps
+ bms.each {|bm| sdisk.bitmap(bm['name'], :remove => '') } unless bms.nil?
- sdisk.bitmap("one-#{@vid}-0", :add => '')
+ sdisk.bitmap("one-#{@vid}-0", :add => '')
+ when :snapshot
+ if snap_folder(did, false)
+ sdisk.read_paths("#{@vm_dir}/disk.#{did}", disk_opts)
+
+ base = sdisk.path
+ elsif sdisk.name != '0'
+ base = sdisk['full-backing-filename']
+
+ sdisk.commit(:q => '')
+
+ FileUtils.rm(sdisk.path, :force => true)
+ else
+ base = sdisk.path
+ end
+
+ ndisk = QemuImg.new("#{sdisk.snap}/1")
+
+ ndisk.create(:f => 'qcow2', :o => 'backing_fmt=qcow2', :b => base)
+
+ symlink("#{sdisk.slink}/1", "#{@vm_dir}/disk.#{did}")
+ end
end
log("[BCK]: Full backup done in #{Time.now - init}s")
@@ -834,6 +1085,42 @@ class KVMDomain
"nbd+unix:///#{target}?socket=#{@socket}"
end
+ # Symlink files (make it succeed by removing the new_path first)
+ def symlink(exist_path, new_path)
+ FileUtils.rm(new_path, :force => true)
+
+ File.symlink(exist_path, new_path)
+ end
+
+ # Create the snap folder structure in the first backup (SSH driver)
+ def snap_folder(disk_id, live)
+ snap_dir = "#{@vm_dir}/disk.#{disk_id}.snap"
+ disk_path = "#{@vm_dir}/disk.#{disk_id}"
+
+ return false if File.ftype(disk_path) == 'link'
+
+ FileUtils.mkdir(snap_dir)
+
+ if live
+ opts = {
+ :path => disk_path,
+ :dest => "#{snap_dir}/0",
+ :pivot => '',
+ :format => 'qcow2'
+ }
+
+ FileUtils.touch "#{snap_dir}/0"
+
+ cmd("#{virsh} blockcopy", @dom.to_s, opts)
+ else
+ FileUtils.mv(disk_path, "#{snap_dir}/0")
+ end
+
+ symlink("disk.#{disk_id}.snap/0", disk_path)
+
+ return true
+ end
+
#---------------------------------------------------------------------------
# Start a Backup operation on the domain (See make_backup_live)
#---------------------------------------------------------------------------
@@ -955,7 +1242,10 @@ begin
# changes and cleans snapshot.
#---------------------------------------------------------------------------
if stop
- vm.stop_backup_full_live(disk) if vm.parent_id == -1 && live
+ if vm.parent_id == -1 && live && @inc_mode != :snapshot
+ vm.stop_backup_full_live(disk)
+ end
+
exit(0)
end
@@ -995,18 +1285,22 @@ begin
vm.clean_checkpoints(disk, true)
vm.backup_full_live(disk)
- else
+ elsif vm.inc_mode == :cbt
vm.define_parent(disk)
vm.backup_nbd_live(disk)
vm.clean_checkpoints(disk)
+ else # vm.inc_mode == :snapshot
+ vm.backup_snapshot_live(disk)
end
else
if vm.parent_id == -1
vm.backup_full(disk)
- else
+ elsif vm.inc_mode == :cbt
vm.backup_nbd(disk)
+ else # vm.inc_mode == :snapshot
+ vm.backup_snapshot(disk)
end
end
rescue StandardError => e
diff --git a/src/tm_mad/ssh/mvds b/src/tm_mad/ssh/mvds
index e07af845c8..735c5f2be1 100755
--- a/src/tm_mad/ssh/mvds
+++ b/src/tm_mad/ssh/mvds
@@ -58,14 +58,18 @@ SRC_DIR="$(dirname $SRC_PATH)"
# and delete base + base.1, snap link and snap dir (if empty)
#-------------------------------------------------------------------------------
CONVERT_CMD=$(cat <vector_value("INCREMENT_MODE");
+
+ one_util::toupper(sattr);
+
+ if ( !sattr.empty() )
+ {
+ if ((sattr != "CBT") && (sattr != "SNAPSHOT"))
+ {
+ sattr = "CBT";
+ }
+
+ config.replace("INCREMENT_MODE", sattr);
+ }
+ else if (!append)
+ {
+ config.replace("INCREMENT_MODE", "CBT");
+ }
}
}
diff --git a/src/vm/VirtualMachineTemplate.cc b/src/vm/VirtualMachineTemplate.cc
index ab70a3079e..cbf3f0a236 100644
--- a/src/vm/VirtualMachineTemplate.cc
+++ b/src/vm/VirtualMachineTemplate.cc
@@ -252,6 +252,7 @@ std::map> VirtualMachineTemplate::UPDATECON
{ "FS_FREEZE",
"KEEP_LAST",
"BACKUP_VOLATILE",
+ "INCREMENT_MODE",
"MODE"}
}
};
diff --git a/src/vnm_mad/remotes/vxlan/vxlan.rb b/src/vnm_mad/remotes/vxlan/vxlan.rb
index 36f5f680ce..36516c8b2f 100644
--- a/src/vnm_mad/remotes/vxlan/vxlan.rb
+++ b/src/vnm_mad/remotes/vxlan/vxlan.rb
@@ -25,7 +25,7 @@ module VXLAN
############################################################################
def create_vlan_dev
vxlan_mode = conf_attribute(@nic, :vxlan_mode, 'multicast')
- group = ""
+ group = ''
if vxlan_mode.downcase == 'evpn'
vxlan_tep = conf_attribute(@nic, :vxlan_tep, 'dev')
@@ -37,9 +37,9 @@ module VXLAN
end
else
begin
- ipaddr = IPAddr.new conf_attribute(@nic, :vxlan_mc, '239.0.0.0')
- rescue
- ipaddr = IPAddr.new '239.0.0.0'
+ ipaddr = IPAddr.new conf_attribute(@nic, :vxlan_mc, '239.0.0.0')
+ rescue StandardError
+ ipaddr = IPAddr.new '239.0.0.0'
end
mc = ipaddr.to_i + @nic[@attr_vlan_id].to_i
@@ -50,16 +50,16 @@ module VXLAN
end
mtu = @nic[:mtu] ? "mtu #{@nic[:mtu]}" : "mtu #{@nic[:conf][:vxlan_mtu]}"
- ttl = @nic[:conf][:vxlan_ttl] ? "ttl #{@nic[:conf][:vxlan_ttl]}" : ""
+ ttl = @nic[:conf][:vxlan_ttl] ? "ttl #{@nic[:conf][:vxlan_ttl]}" : ''
- ip_link_conf = ""
+ ip_link_conf = ''
@nic[:ip_link_conf].each do |option, value|
case value
when true
- value = "on"
+ value = 'on'
when false
- value = "off"
+ value = 'off'
end
ip_link_conf << "#{option} #{value} "