Allow to use many disks in vagrant box for libvirt provider

Adds support for a new multi disk box format and handling to upload the
multiple disks to the storage pool.

New format is:
{
  'disks': [
    {
      'name': 'disk1.img',
      'virtual_size': 10,
      'format': 'qcow2'
    },
    {
      'name': 'disk2.img',
      'virtual_size': 15,
      'format': 'qcow2'
    },
    {
      'name': 'disk3.img',
    }
  ],
  'provider': 'libvirt',
  'format': 'qcow2'
}

It is expected to remove format from being set at the top level when
using the new format, with the assuming that qcow2 should be the default
format, and other formats should be permitted to be specified as needed.

Includes tests for handling the box images and creation of domain
volumes. Additionally includes an integration test to ensure a box with
2 disks will work as expected.

Partially fixes: #602
This commit is contained in:
Richard Turc
2020-09-10 10:03:00 +02:00
committed by Darragh Bailey
parent 6f608c54bf
commit 225237b125
19 changed files with 802 additions and 144 deletions

View File

@@ -92,6 +92,7 @@ module VagrantPlugins
# Storage
@storage_pool_name = config.storage_pool_name
@snapshot_pool_name = config.snapshot_pool_name
@domain_volumes = []
@disks = config.disks
@cdroms = config.cdroms
@@ -141,19 +142,28 @@ module VagrantPlugins
else
pool_name = @storage_pool_name
end
@logger.debug "Search for volume in pool: #{pool_name}"
domain_volume = env[:machine].provider.driver.connection.volumes.all(
name: "#{@name}.img"
).find { |x| x.pool_name == pool_name }
raise Errors::DomainVolumeExists if domain_volume.nil?
@domain_volume_path = domain_volume.path
@logger.debug "Search for volumes in pool: #{pool_name}"
env[:box_volumes].each_index do |index|
suffix_index = index > 0 ? "_#{index}" : ''
domain_volume = env[:machine].provider.driver.connection.volumes.all(
name: "#{@name}#{suffix_index}.img"
).find { |x| x.pool_name == pool_name }
raise Errors::DomainVolumeExists if domain_volume.nil?
@domain_volumes.push({
:dev => (index+1).vdev.to_s,
:cache => @domain_volume_cache,
:bus => @disk_bus,
:path => domain_volume.path,
:virtual_size => env[:box_volumes][index][:virtual_size]
})
end
end
# If we have a box, take the path from the domain volume and set our storage_prefix.
# If not, we dump the storage pool xml to get its defined path.
# the default storage prefix is typically: /var/lib/libvirt/images/
if env[:machine].config.vm.box
storage_prefix = File.dirname(@domain_volume_path) + '/' # steal
storage_prefix = File.dirname(@domain_volumes[0][:path]) + '/' # steal
else
storage_prefix = get_disk_storage_prefix(env, @storage_pool_name)
end
@@ -250,7 +260,9 @@ module VagrantPlugins
env[:ui].info(" -- Base box: #{env[:machine].box.name}")
end
env[:ui].info(" -- Storage pool: #{@storage_pool_name}")
env[:ui].info(" -- Image: #{@domain_volume_path} (#{env[:box_virtual_size]}G)")
@domain_volumes.each do |volume|
env[:ui].info(" -- Image(#{volume[:device]}): #{volume[:path]}, #{volume[:virtual_size]}G")
end
if not @disk_driver_opts.empty?
env[:ui].info(" -- Disk driver opts: #{@disk_driver_opts.reject { |k,v| v.nil? }.map { |k,v| "#{k}='#{v}'"}.join(' ')}")

View File

@@ -18,72 +18,74 @@ module VagrantPlugins
def call(env)
env[:ui].info(I18n.t('vagrant_libvirt.creating_domain_volume'))
# Get config options.
config = env[:machine].provider_config
env[:box_volumes].each_index do |index|
suffix_index = index > 0 ? "_#{index}" : ''
# Get config options.
config = env[:machine].provider_config
# This is name of newly created image for vm.
@name = "#{env[:domain_name]}.img"
# This is name of newly created image for vm.
@name = "#{env[:domain_name]}#{suffix_index}.img"
# Verify the volume doesn't exist already.
domain_volume = env[:machine].provider.driver.connection.volumes.all(
name: @name
).first
raise Errors::DomainVolumeExists if domain_volume && domain_volume.id
# Verify the volume doesn't exist already.
domain_volume = env[:machine].provider.driver.connection.volumes.all(
name: @name
).first
raise Errors::DomainVolumeExists if domain_volume && domain_volume.id
# Get path to backing image - box volume.
box_volume = env[:machine].provider.driver.connection.volumes.all(
name: env[:box_volume_name]
).first
@backing_file = box_volume.path
# Get path to backing image - box volume.
box_volume = env[:machine].provider.driver.connection.volumes.all(
name: env[:box_volumes][index][:name]
).first
@backing_file = box_volume.path
# Virtual size of image. Take value worked out by HandleBoxImage
@capacity = env[:box_virtual_size] # G
# Virtual size of image. Take value worked out by HandleBoxImage
@capacity = env[:box_volumes][index][:virtual_size] # G
# Create new volume from xml template. Fog currently doesn't support
# volume snapshots directly.
begin
xml = Nokogiri::XML::Builder.new do |xml|
xml.volume do
xml.name(@name)
xml.capacity(@capacity, unit: 'G')
xml.target do
xml.format(type: 'qcow2')
xml.permissions do
xml.owner storage_uid(env)
xml.group storage_gid(env)
xml.label 'virt_image_t'
end
end
xml.backingStore do
xml.path(@backing_file)
xml.format(type: 'qcow2')
xml.permissions do
xml.owner storage_uid(env)
xml.group storage_gid(env)
xml.label 'virt_image_t'
# Create new volume from xml template. Fog currently doesn't support
# volume snapshots directly.
begin
xml = Nokogiri::XML::Builder.new do |xml|
xml.volume do
xml.name(@name)
xml.capacity(@capacity, unit: 'G')
xml.target do
xml.format(type: 'qcow2')
xml.permissions do
xml.owner storage_uid(env)
xml.group storage_gid(env)
xml.label 'virt_image_t'
end
end
xml.backingStore do
xml.path(@backing_file)
xml.format(type: 'qcow2')
xml.permissions do
xml.owner storage_uid(env)
xml.group storage_gid(env)
xml.label 'virt_image_t'
end
end
end
end.to_xml(
save_with: Nokogiri::XML::Node::SaveOptions::NO_DECLARATION |
Nokogiri::XML::Node::SaveOptions::NO_EMPTY_TAGS |
Nokogiri::XML::Node::SaveOptions::FORMAT
)
if config.snapshot_pool_name != config.storage_pool_name
pool_name = config.snapshot_pool_name
else
pool_name = config.storage_pool_name
end
end.to_xml(
save_with: Nokogiri::XML::Node::SaveOptions::NO_DECLARATION |
Nokogiri::XML::Node::SaveOptions::NO_EMPTY_TAGS |
Nokogiri::XML::Node::SaveOptions::FORMAT
)
if config.snapshot_pool_name != config.storage_pool_name
pool_name = config.snapshot_pool_name
else
pool_name = config.storage_pool_name
@logger.debug "Using pool #{pool_name} for base box snapshot"
domain_volume = env[:machine].provider.driver.connection.volumes.create(
xml: xml,
pool_name: pool_name
)
rescue Fog::Errors::Error => e
raise Errors::FogDomainVolumeCreateError,
error_message: e.message
end
@logger.debug "Using pool #{pool_name} for base box snapshot"
domain_volume = env[:machine].provider.driver.connection.volumes.create(
xml: xml,
pool_name: pool_name
)
rescue Fog::Errors::Error => e
raise Errors::FogDomainVolumeCreateError,
error_message: e.message
end
@app.call(env)
end
end

View File

@@ -18,31 +18,46 @@ module VagrantPlugins
def call(env)
# Verify box metadata for mandatory values.
#
# Virtual size has to be set for allocating space in storage pool.
box_virtual_size = env[:machine].box.metadata['virtual_size']
raise Errors::NoBoxVirtualSizeSet if box_virtual_size.nil?
# Verify disk number
disks = env[:machine].box.metadata.fetch('disks', [])
if disks.empty?
disks.push({
'path' => HandleBoxImage.get_default_box_image_path(0),
'name' => HandleBoxImage.get_volume_name(env, 0),
'virtual_size' => HandleBoxImage.get_virtual_size(env),
})
end
HandleBoxImage.verify_virtual_size_in_disks(disks)
# Support qcow2 format only for now, but other formats with backing
# store capability should be usable.
box_format = env[:machine].box.metadata['format']
if box_format.nil?
raise Errors::NoBoxFormatSet
elsif box_format != 'qcow2'
raise Errors::WrongBoxFormatSet
end
HandleBoxImage.verify_box_format(box_format)
env[:box_volume_number] = disks.length()
env[:box_volumes] = Array.new(env[:box_volume_number]) {|i| {
:path => HandleBoxImage.get_box_image_path(
env,
disks[i].fetch('path', HandleBoxImage.get_default_box_image_path(i))
),
:name => disks[i].fetch('name', HandleBoxImage.get_volume_name(env, i)),
:virtual_size => disks[i]['virtual_size'],
:format => HandleBoxImage.verify_box_format(
disks[i].fetch('format', box_format),
i
)
}
}
# Get config options
config = env[:machine].provider_config
box_image_file = env[:machine].box.directory.join('box.img').to_s
env[:box_volume_name] = env[:machine].box.name.to_s.dup.gsub('/', '-VAGRANTSLASH-')
env[:box_volume_name] << "_vagrant_box_image_#{
begin
env[:machine].box.version.to_s
rescue
''
end}.img"
box_image_files = []
env[:box_volumes].each do |d|
box_image_files.push(d[:path])
end
# Override box_virtual_size
box_virtual_size = env[:box_volumes][0][:virtual_size]
if config.machine_virtual_size
if config.machine_virtual_size < box_virtual_size
# Warn that a virtual size less than the box metadata size
@@ -57,70 +72,20 @@ module VagrantPlugins
end
end
# save for use by later actions
env[:box_virtual_size] = box_virtual_size
env[:box_volumes][0][:virtual_size] = box_virtual_size
# while inside the synchronize block take care not to call the next
# action in the chain, as must exit this block first to prevent
# locking all subsequent actions as well.
@@lock.synchronize do
# Don't continue if image already exists in storage pool.
box_volume = env[:machine].provider.driver.connection.volumes.all(
name: env[:box_volume_name]
).first
break if box_volume && box_volume.id
env[:box_volumes].each_index do |i|
# Don't continue if image already exists in storage pool.
box_volume = env[:machine].provider.driver.connection.volumes.all(
name: env[:box_volumes][i][:name]
).first
next if box_volume && box_volume.id
# Box is not available as a storage pool volume. Create and upload
# it as a copy of local box image.
env[:ui].info(I18n.t('vagrant_libvirt.uploading_volume'))
# Create new volume in storage pool
unless File.exist?(box_image_file)
raise Vagrant::Errors::BoxNotFound, name: env[:machine].box.name
end
box_image_size = File.size(box_image_file) # B
message = "Creating volume #{env[:box_volume_name]}"
message << " in storage pool #{config.storage_pool_name}."
@logger.info(message)
@storage_volume_uid = storage_uid env
@storage_volume_gid = storage_gid env
begin
fog_volume = env[:machine].provider.driver.connection.volumes.create(
name: env[:box_volume_name],
allocation: "#{box_image_size / 1024 / 1024}M",
capacity: "#{box_virtual_size}G",
format_type: box_format,
owner: @storage_volume_uid,
group: @storage_volume_gid,
pool_name: config.storage_pool_name
)
rescue Fog::Errors::Error => e
raise Errors::FogCreateVolumeError,
error_message: e.message
end
# Upload box image to storage pool
ret = upload_image(box_image_file, config.storage_pool_name,
env[:box_volume_name], env) do |progress|
rewriting(env[:ui]) do |ui|
ui.clear_line
ui.report_progress(progress, box_image_size, false)
end
end
# Clear the line one last time since the progress meter doesn't
# disappear immediately.
rewriting(env[:ui]) {|ui| ui.clear_line}
# If upload failed or was interrupted, remove created volume from
# storage pool.
if env[:interrupted] || !ret
begin
fog_volume.destroy
rescue
nil
end
send_box_image(env, config, box_image_files[i], env[:box_volumes][i])
end
end
@@ -129,6 +94,105 @@ module VagrantPlugins
protected
def self.get_volume_name(env, index)
name = env[:machine].box.name.to_s.dup.gsub('/', '-VAGRANTSLASH-')
name << "_vagrant_box_image_#{
begin
env[:machine].box.version.to_s
rescue
''
end}_#{index}.img"
return name
end
def self.get_virtual_size(env)
# Virtual size has to be set for allocating space in storage pool.
box_virtual_size = env[:machine].box.metadata['virtual_size']
raise Errors::NoBoxVirtualSizeSet if box_virtual_size.nil?
return box_virtual_size
end
def self.get_default_box_image_path(index)
return index <= 0 ? 'box.img' : "box_#{index}.img"
end
def self.get_box_image_path(env, box_name)
return env[:machine].box.directory.join(box_name).to_s
end
def self.verify_box_format(box_format, disk_index=nil)
if box_format.nil?
raise Errors::NoBoxFormatSet
elsif box_format != 'qcow2'
if disk_index.nil?
raise Errors::WrongBoxFormatSet
else
raise Errors::WrongDiskFormatSet,
disk_index: disk_index
end
end
return box_format
end
def self.verify_virtual_size_in_disks(disks)
disks.each_with_index do |disk, index|
raise Errors::NoDiskVirtualSizeSet, disk_index:index if disk['virtual_size'].nil?
end
end
def send_box_image(env, config, box_image_file, box_volume)
# Box is not available as a storage pool volume. Create and upload
# it as a copy of local box image.
env[:ui].info(I18n.t('vagrant_libvirt.uploading_volume'))
# Create new volume in storage pool
unless File.exist?(box_image_file)
raise Vagrant::Errors::BoxNotFound, name: env[:machine].box.name
end
box_image_size = File.size(box_image_file) # B
message = "Creating volume #{box_volume[:name]}"
message << " in storage pool #{config.storage_pool_name}."
@logger.info(message)
begin
fog_volume = env[:machine].provider.driver.connection.volumes.create(
name: box_volume[:name],
allocation: "#{box_image_size / 1024 / 1024}M",
capacity: "#{box_volume[:virtual_size]}G",
format_type: box_volume[:format],
owner: storage_uid(env),
group: storage_gid(env),
pool_name: config.storage_pool_name
)
rescue Fog::Errors::Error => e
raise Errors::FogCreateVolumeError,
error_message: e.message
end
# Upload box image to storage pool
ret = upload_image(box_image_file, config.storage_pool_name,
box_volume[:name], env) do |progress|
rewriting(env[:ui]) do |ui|
ui.clear_line
ui.report_progress(progress, box_image_size, false)
end
end
# Clear the line one last time since the progress meter doesn't
# disappear immediately.
rewriting(env[:ui]) {|ui| ui.clear_line}
# If upload failed or was interrupted, remove created volume from
# storage pool.
if env[:interrupted] || !ret
begin
fog_volume.destroy
rescue
nil
end
end
end
# Fog Libvirt currently doesn't support uploading images to storage
# pool volumes. Use ruby-libvirt client instead.
def upload_image(image_file, pool_name, volume_name, env)

View File

@@ -46,6 +46,10 @@ module VagrantPlugins
error_key(:no_box_virtual_size)
end
class NoDiskVirtualSizeSet < VagrantLibvirtError
error_key(:no_disk_virtual_size)
end
class NoBoxFormatSet < VagrantLibvirtError
error_key(:no_box_format)
end
@@ -54,6 +58,10 @@ module VagrantPlugins
error_key(:wrong_box_format)
end
class WrongDiskFormatSet < VagrantLibvirtError
error_key(:wrong_disk_format)
end
# Fog Libvirt exceptions
class FogError < VagrantLibvirtError
error_key(:fog_error)

View File

@@ -113,18 +113,18 @@
<% if @emulator_path %>
<emulator><%= @emulator_path %></emulator>
<% end %>
<% if @domain_volume_path %>
<% @domain_volumes.each do |volume| -%>
<disk type='file' device='disk'>
<driver name='qemu' type='qcow2' <%=
@disk_driver_opts.empty? ? "cache='#{@domain_volume_cache}'" :
@disk_driver_opts.empty? ? "cache='#{volume[:cache]}'" :
@disk_driver_opts.reject { |k,v| v.nil? }
.map { |k,v| "#{k}='#{v}'"}
.join(' ') -%>/>
<source file='<%= @domain_volume_path %>'/>
<source file='<%= volume[:path] %>'/>
<%# we need to ensure a unique target dev -%>
<target dev='<%= @disk_device %>' bus='<%= @disk_bus %>'/>
<target dev='<%= volume[:dev] %>' bus='<%= volume[:bus] %>'/>
</disk>
<% end %>
<% end -%>
<%# additional disks -%>
<% @disks.each do |d| -%>
<disk type='file' device='disk'>