For ONE 5.10.3
Default POOL_DIR is ‘/tank/one/lxd’
tar.rb
#!/usr/bin/ruby
# -------------------------------------------------------------------------- #
# Copyright 2002-2019, 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. #
#--------------------------------------------------------------------------- #
$LOAD_PATH.unshift File.dirname(__FILE__)
require 'mapper'
# Tar file mapping for system images, backed by tar and btrfs/zfs
class TarMapper < Mapper
POOL_DIR = '/tank/one/lxd'
COMMANDS = COMMANDS.merge({
:btrfs => 'sudo btrfs',
:zfs => 'sudo zfs',
:tar => 'sudo tar',
:rm => 'sudo rm',
:chown => 'sudo chown',
:stat => 'stat',
:sha256sum => 'sha256sum'
})
def initialize
@has_pigz = File.exists?('/usr/bin/pigz')
@has_pbzip2 = File.exists?('/usr/bin/pbzip2')
@has_pixz = File.exists?('/usr/bin/pixz')
@pool_fs = fs_type(POOL_DIR)
end
def do_map(one_vm, disk, _directory)
dsrc = one_vm.disk_source(disk)
disk_id = disk['DISK_ID']
path = target_path(one_vm.vm_name, disk_id)
# If the path is already exists, the disk should be already unpacked
return path if File.exists?(path)
self.send(@pool_fs+"_unpack", dsrc, path)
end
def do_unmap(device, one_vm, disk, _directory)
umount_dev(_directory)
end
def map(one_vm, disk, directory)
return true if mount_on?(directory)
device = do_map(one_vm, disk, directory)
OpenNebula.log_info "Mapping disk at #{directory} using device #{device}"
return false unless device
return mount_dev(device, directory)
end
def unmap(one_vm, disk, directory)
OpenNebula.log_info "Unmapping disk at #{directory}"
real_path = directory
is_rootfs = real_path =~ %r{.*/rootfs}
is_shared_ds = File.symlink?(one_vm.sysds_path)
real_path = File.realpath(directory) if !is_rootfs && is_shared_ds
device = one_vm.disk_source(disk)
return unless do_unmap(device, one_vm, disk, real_path)
true
end
def save(one_vm, disk, directory)
dsrc = one_vm.disk_source(disk)
disk_id = disk['DISK_ID']
path = target_path(one_vm.vm_name, disk_id)
return unless File.exists?(path)
OpenNebula.log_info "Saving directory #{directory} back to disk.#{disk_id}"
self.send(@pool_fs+"_pack", dsrc, path)
end
private
def target_path(name, disk)
POOL_DIR + "/containers/#{name}/disk.#{disk}/"
end
def get_vm_path(path)
path.sub(/(containers\/.+?)\/.*$/, '\1')
end
def btrfs_pack(src, dst)
OpenNebula.log_info "Packing of #{dst} back to #{src}"
return false unless tar(src, dst)
# Recursively delete subvols
subvols = btrfs_get_subvols(dst)
subvols.each do |vol|
btrfs_subvol_delete(vol)
end
# Delete root dir if no subvols left
vm_path = get_vm_path(dst)
if File.exists?(vm_path) && dir_empty?(vm_path)
btrfs_subvol_delete(vm_path)
end
true
end
def btrfs_unpack(src, dst)
unless File.exists?(POOL_DIR + '/containers/')
OpenNebula.log_info "Creating subvolume for containers at " + POOL_DIR + "/containers/"
return false unless btrfs_subvol_create(POOL_DIR+"/containers/")
end
vm_path = get_vm_path(dst)
unless File.exists?(vm_path)
return false unless btrfs_subvol_create(vm_path)
end
# Speed hack using subvolumes
if hash = cksum(src)
unless File.exists?(POOL_DIR + '/images/')
OpenNebula.log_info "Creating subvolume for images at " + POOL_DIR + "/images/"
return false unless btrfs_subvol_create(POOL_DIR+"/images/")
end
# Create image snapshot first
image = POOL_DIR + "/images/#{hash}"
unless File.exists?(image)
OpenNebula.log_info "Creating subvolume for disk #{src} at #{image}"
tmp = POOL_DIR + "/tmp/"
return false unless btrfs_subvol_delete(tmp)
return false unless btrfs_subvol_create(tmp)
OpenNebula.log_info "Unpacking disk #{src} to #{image}"
return false unless untar(src, tmp)
return false unless btrfs_snapshot(tmp, image, true)
return false unless btrfs_subvol_delete(tmp)
end
# Instantiate
OpenNebula.log_info "Creating image instance of #{src} at #{dst}"
return false unless btrfs_snapshot(image, dst)
else
# Just unpack the image
OpenNebula.log_info "Creating image instance of #{src} at #{dst}"
return false unless btrfs_subvol_create(dst)
return false unless untar(src, dst)
end
dst
end
def zfs_pack(src, dst)
OpenNebula.log_info "Packing of #{dst} back to #{src}"
return false unless tar(src, dst)
return false unless zfs_delete_subvol(dst)
# Delete root dir if no subvols left
vm_path = get_vm_path(dst)
if File.exists?(vm_path) && dir_empty?(vm_path)
zfs_delete_subvol(vm_path)
end
true
end
def zfs_unpack(src, dst)
unless File.exists?(POOL_DIR + '/containers/')
OpenNebula.log_info "Creating subvolume for containers at " + POOL_DIR + "/containers/"
return false unless zfs_subvol_create(POOL_DIR+"/containers/")
end
vm_path = get_vm_path(dst)
unless File.exists?(vm_path)
return false unless zfs_subvol_create(vm_path)
end
# Just unpack the image
OpenNebula.log_info "Creating image instance of #{src} at #{dst}"
return false unless zfs_subvol_create(dst)
return false unless untar(src, dst)
dst
end
def dir_pack(src, dst)
OpenNebula.log_info "Packing of #{dst} back to #{src}"
return false unless tar(src, dst)
return false unless dir_delete(dst)
# Delete root dir if no subdirs left
vm_path = get_vm_path(dst)
if File.exists?(vm_path) && dir_empty?(vm_path)
dir_delete(vm_path)
end
true
end
def dir_unpack(src, dst)
OpenNebula.log_info "Creating image instance of #{src} at #{dst}"
return false unless mkdirp_safe(dst)
return false unless untar(src, dst)
dst
end
def dir_delete(path)
return true unless File.exists?(path)
OpenNebula.log_info "Deleting #{path}"
return false unless path_sanity_check(path)
rc, out, err = Command.execute("#{COMMANDS[:rm]} -rf #{path}", false)
unless rc.zero?
OpenNebula.log_error("#{__method}: #{err}")
return false
end
true
end
def btrfs_subvol_create(path)
OpenNebula.log_info "Creating btrfs subvolume #{path}"
return false unless path_sanity_check(path)
rc, out, err = Command.execute("#{COMMANDS[:btrfs]} subvolume create #{path}", false)
unless rc.zero?
OpenNebula.log_error("#{__method__}: #{err}")
return false
end
true
end
def btrfs_get_subvols(path)
rc, out, err = Command.execute("#{COMMANDS[:btrfs]} inspect-internal rootid #{path}", false)
unless rc.zero?
OpenNebula.log_error("#{__method__}: #{err}")
return false
end
rootid = out.chomp.to_i
subvols = {rootid => path}
rc, out, err = Command.execute("#{COMMANDS[:btrfs]} subvolume list -p #{path}", false)
unless rc.zero?
OpenNebula.log_error("#{__method__}: #{err}")
return false
end
out.split(/\n/).each do |line|
line.match(/ID (\d+) .*parent (\d+) .*path (.*)$/) do |m|
if subvols.key?(m[2].to_i)
subvols[m[1].to_i] = File.join(path, m[3])
end
end
end
subvols.sort_by { |k, v| k }.reverse.map {|k, v| v }
end
def btrfs_subvol_delete(path)
return true unless File.exists?(path)
OpenNebula.log_info "Deleting btrfs subvolume #{path}"
return false unless path_sanity_check(path)
rc, out, err = Command.execute("#{COMMANDS[:btrfs]} subvolume delete #{path}", false)
unless rc.zero?
OpenNebula.log_error("#{__method__}: #{err}")
return false
end
true
end
def btrfs_snapshot(src, dst, readonly=false)
OpenNebula.log_info "Creating btrfs snapshot of #{src} to #{dst}"
return false unless path_sanity_check(src) && path_sanity_check(dst)
arg = readonly ? "-r" : ""
rc, out, err = Command.execute("#{COMMANDS[:btrfs]} subvolume snapshot #{arg} #{src} #{dst}", false)
unless rc.zero?
OpenNebula.log_error("#{__method__}: #{err}")
return false
end
true
end
def zfs_subvol_create(path)
return false unless path_sanity_check(path)
# Change /tank/ to tank to comply with ZFS
pool = path.sub(/^\//, '').sub(/\/$/, '')
OpenNebula.log_info "Creating zfs subvolume #{pool}"
rc, out, err = Command.execute("#{COMMANDS[:zfs]} create #{pool}", false)
unless rc.zero?
OpenNebula.log_error("#{__method__}: #{err}")
return false
end
true
end
def zfs_subvol_delete(path)
return true unless File.exists?(path)
return false unless path_sanity_check(path)
# Change /tank/ to tank to comply with ZFS
pool = path.sub(/^\//, '').sub(/\/$/, '')
OpenNebula.log_info "Deleting zfs subvolume #{pool}"
rc, out, err = Command.execute("#{COMMANDS[:zfs]} destroy #{pool}", false)
unless rc.zero?
OpenNebula.log_error("#{__method__}: #{err}")
return false
end
true
end
def path_sanity_check(path)
if path.empty? or !path.start_with?(POOL_DIR)
OpenNebula.log_error("#{__method__}: Path not within #{POOL_DIR}")
return false
end
true
end
def mkdirp_safe(path)
return false unless path_sanity_check(path)
rc, _out, err = Command.execute("#{COMMANDS[:su_mkdir]} -p #{path}", false)
return true if rc.zero?
OpenNebula.log_error("#{__method__}: #{err}")
false
end
def cksum(path)
# Do not checksum files larger than 4 GB
if File.size(path) <= 4294967296
rc, out, err = Command.execute("#{COMMANDS[:sha256sum]} -b #{path}", false)
unless rc.zero?
OpenNebula.log_error("#{__method__}: #{err}")
return false
end
if out.length < 64
OpenNebula.log_error("#{__method__}: #{err}")
return false
end
out[0..63]
else
false
end
end
def tar(file, path)
return false unless path_sanity_check(path)
driver = disk_type(file)
c = case driver
when :bzip2
@has_pbzip2 ? '-I pbzip2' : '-j'
when :gzip
@has_pigz ? '-I pigz' : '-z'
when :xz
@has_pixz ? '-I pixz' : '-J'
else
''
end
# Aim for faster compression
env = case driver
when :bzip2
"BZIP2=-1"
when :gzip
"GZIP=-1"
when :xz
"XZ_OPT=-1"
else
""
end
rc, out, err = Command.execute("#{env} #{COMMANDS[:tar]} #{c} --numeric-owner -Scf #{file} -C #{path} .", false)
unless rc.zero?
OpenNebula.log_error("#{__method__}: #{err}")
return false
end
true
end
def untar(file, path)
return false unless path_sanity_check(path)
c = case disk_type(file)
when :bzip2
@has_pbzip2 ? '-I pbzip2' : '-j'
when :gzip
@has_pigz ? '-I pigz' : '-z'
when :xz
@has_pixz ? '-I pixz' : '-J'
else
''
end
rc, out, err = Command.execute("#{COMMANDS[:tar]} #{c} -xif #{file} -C #{path}", false)
unless rc.zero?
OpenNebula.log_error("#{__method__}: #{err}")
return false
end
true
end
def disk_type(ds)
rc, out, err = Command.execute("#{COMMANDS[:file]} #{ds}", false)
unless rc.zero?
OpenNebula.log_error("#{__method__} #{err}")
return
end
case out
when /.*tar archive.*/
:tar
when /.*XZ compressed data.*/
:xz
when /.*bzip2 compressed data.*/
:bzip2
when /.*gzip compressed data.*/
:gzip
else
OpenNebula.log("Unknown #{out} image format")
nil
end
end
def dir_empty?(path)
(Dir.entries(path) - %w{. ..}).empty?
end
def mount_on?(path)
_rc, out, _err = Command.execute("#{COMMANDS[:mount]}", false)
if out.match(/ on #{path}/)
OpenNebula.log_error("#{__method__}: Mount detected in #{path}")
return true
end
false
end
def mount_dev(dev, path)
OpenNebula.log_info "Mounting #{dev} at #{path}"
mkdir_safe(path)
rc, _out, err = Command.execute("#{COMMANDS[:mount]} #{dev} #{path} -o bind", true)
if rc != 0
OpenNebula.log_error("mount_dev: #{err}")
return false
end
file = File.stat(dev)
rc, _out, err = Command.execute("#{COMMANDS[:chown]} #{file.uid}:#{file.gid} #{path}", true)
if rc != 0
OpenNebula.log_error("chown_path: #{err}")
return false
end
true
end
def fs_type(path)
_rc, out, _err = Command.execute("#{COMMANDS[:stat]} --file-system --format=%T #{path}", false)
if out.match(/btrfs|zfs/)
out.chomp
else
'dir'
end
end
end
container.rb
def new_disk_mapper(disk)
...
when /.*gzip compressed data.*/, /.*bzip2 compressed data.*/, /.*XZ compressed data.*/, /.*tar archive.*/
OpenNebula.log "Using tar disk mapper for #{ds}"
TarMapper.new