From 225237b12557360cf8e2345c270363aef47bc6e6 Mon Sep 17 00:00:00 2001 From: Richard Turc Date: Thu, 10 Sep 2020 10:03:00 +0200 Subject: [PATCH] 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 --- .github/workflows/integration-tests.yml | 1 + lib/vagrant-libvirt/action/create_domain.rb | 28 +- .../action/create_domain_volume.rb | 114 +++---- .../action/handle_box_image.rb | 212 ++++++++----- lib/vagrant-libvirt/errors.rb | 8 + lib/vagrant-libvirt/templates/domain.xml.erb | 10 +- locales/en.yml | 4 + spec/unit/action/create_domain_spec.rb | 6 + spec/unit/action/create_domain_volume_spec.rb | 102 ++++++ .../one_disk_in_storage.xml | 21 ++ .../three_disks_in_storage_disk_0.xml | 21 ++ .../three_disks_in_storage_disk_1.xml | 21 ++ .../three_disks_in_storage_disk_2.xml | 21 ++ spec/unit/action/handle_box_image_spec.rb | 296 ++++++++++++++++++ spec/unit/templates/domain_all_settings.xml | 5 + spec/unit/templates/domain_spec.rb | 20 +- tests/runtests.bats | 14 + tests/two_disks/Vagrantfile | 10 + tools/create_box_with_two_disks.sh | 32 ++ 19 files changed, 802 insertions(+), 144 deletions(-) create mode 100644 spec/unit/action/create_domain_volume_spec.rb create mode 100644 spec/unit/action/create_domain_volume_spec/one_disk_in_storage.xml create mode 100644 spec/unit/action/create_domain_volume_spec/three_disks_in_storage_disk_0.xml create mode 100644 spec/unit/action/create_domain_volume_spec/three_disks_in_storage_disk_1.xml create mode 100644 spec/unit/action/create_domain_volume_spec/three_disks_in_storage_disk_2.xml create mode 100644 spec/unit/action/handle_box_image_spec.rb create mode 100644 tests/two_disks/Vagrantfile create mode 100755 tools/create_box_with_two_disks.sh diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml index 920b365..bdb114e 100644 --- a/.github/workflows/integration-tests.yml +++ b/.github/workflows/integration-tests.yml @@ -18,6 +18,7 @@ jobs: - simple vm provision via shell - bring up with custom default prefix - bring up with second disk + - bring up with two disks - bring up with adjusted memory settings - bring up with adjusted cpu settings - ip is reachable with private network diff --git a/lib/vagrant-libvirt/action/create_domain.rb b/lib/vagrant-libvirt/action/create_domain.rb index fa70fb6..eac27f6 100644 --- a/lib/vagrant-libvirt/action/create_domain.rb +++ b/lib/vagrant-libvirt/action/create_domain.rb @@ -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(' ')}") diff --git a/lib/vagrant-libvirt/action/create_domain_volume.rb b/lib/vagrant-libvirt/action/create_domain_volume.rb index 092a408..49831b2 100644 --- a/lib/vagrant-libvirt/action/create_domain_volume.rb +++ b/lib/vagrant-libvirt/action/create_domain_volume.rb @@ -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 diff --git a/lib/vagrant-libvirt/action/handle_box_image.rb b/lib/vagrant-libvirt/action/handle_box_image.rb index 066df16..05381ca 100644 --- a/lib/vagrant-libvirt/action/handle_box_image.rb +++ b/lib/vagrant-libvirt/action/handle_box_image.rb @@ -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) diff --git a/lib/vagrant-libvirt/errors.rb b/lib/vagrant-libvirt/errors.rb index 3945c2b..3410a1d 100644 --- a/lib/vagrant-libvirt/errors.rb +++ b/lib/vagrant-libvirt/errors.rb @@ -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) diff --git a/lib/vagrant-libvirt/templates/domain.xml.erb b/lib/vagrant-libvirt/templates/domain.xml.erb index c45df81..240acd0 100644 --- a/lib/vagrant-libvirt/templates/domain.xml.erb +++ b/lib/vagrant-libvirt/templates/domain.xml.erb @@ -113,18 +113,18 @@ <% if @emulator_path %> <%= @emulator_path %> <% end %> - <% if @domain_volume_path %> +<% @domain_volumes.each do |volume| -%> /> - + <%# we need to ensure a unique target dev -%> - + - <% end %> +<% end -%> <%# additional disks -%> <% @disks.each do |d| -%> diff --git a/locales/en.yml b/locales/en.yml index 0bf881b..67a8805 100644 --- a/locales/en.yml +++ b/locales/en.yml @@ -92,10 +92,14 @@ en: Error: %{stderr} no_box_virtual_size: |- No image virtual size specified for box. + no_disk_virtual_size: |- + No image virtual size specified for disk with index %{disk_index}. no_box_format: |- No image format specified for box. wrong_box_format: |- Wrong image format specified for box. + wrong_disk_format: |- + Wrong image format specified for disk with index %{disk_index}. fog_libvirt_connection_error: |- Error while connecting to Libvirt: %{error_message} fog_create_volume_error: |- diff --git a/spec/unit/action/create_domain_spec.rb b/spec/unit/action/create_domain_spec.rb index d85d676..3327680 100644 --- a/spec/unit/action/create_domain_spec.rb +++ b/spec/unit/action/create_domain_spec.rb @@ -31,6 +31,12 @@ describe VagrantPlugins::ProviderLibvirt::Action::CreateDomain do env[:domain_name] = "vagrant-test_default" + env[:box_volumes] = [] + env[:box_volumes].push({ + :path=>"/test/box.img", + :name=>"test_vagrant_box_image_1.1.1_0.img", + :virtual_size=>5 + }) # should be ignored for system session and used for user session allow(Process).to receive(:uid).and_return(9999) allow(Process).to receive(:gid).and_return(9999) diff --git a/spec/unit/action/create_domain_volume_spec.rb b/spec/unit/action/create_domain_volume_spec.rb new file mode 100644 index 0000000..716eaa1 --- /dev/null +++ b/spec/unit/action/create_domain_volume_spec.rb @@ -0,0 +1,102 @@ +require 'spec_helper' +require 'support/sharedcontext' +require 'support/libvirt_context' + +require 'vagrant-libvirt/action/destroy_domain' + +describe VagrantPlugins::ProviderLibvirt::Action::CreateDomainVolume do + subject { described_class.new(app, env) } + + include_context 'unit' + include_context 'libvirt' + + let(:libvirt_domain) { double('libvirt_domain') } + let(:libvirt_client) { double('libvirt_client') } + let(:volumes) { double('volumes') } + let(:all) { double('all') } + let(:box_volume) { double('box_volume') } + + def read_test_file(name) + File.read(File.join(File.dirname(__FILE__), File.basename(__FILE__, '.rb'), name)) + end + + describe '#call' do + before do + allow_any_instance_of(VagrantPlugins::ProviderLibvirt::Driver) + .to receive(:connection).and_return(connection) + allow(connection).to receive(:client).and_return(libvirt_client) + allow(connection).to receive(:volumes).and_return(volumes) + allow(volumes).to receive(:all).and_return(all) + allow(all).to receive(:first).and_return(box_volume) + allow(box_volume).to receive(:id).and_return(nil) + env[:domain_name] = 'test' + end + + context 'when one disk' do + before do + allow(box_volume).to receive(:path).and_return('/test/path_0.img') + env[:box_volumes] = [ + { + :name=>"test_vagrant_box_image_1.1.1_0.img", + :virtual_size=>5 + } + ] + end + + it 'should create one disk in storage' do + expected_xml = read_test_file('one_disk_in_storage.xml') + expect(ui).to receive(:info).with('Creating image (snapshot of base box volume).') + expect(logger).to receive(:debug).with('Using pool default for base box snapshot') + expect(volumes).to receive(:create).with( + :xml => expected_xml, + :pool_name => "default" + ) + expect(subject.call(env)).to be_nil + end + end + + context 'when three disks' do + before do + allow(box_volume).to receive(:path).and_return( + '/test/path_0.img', + '/test/path_1.img', + '/test/path_2.img', + ) + env[:box_volumes] = [ + { + :name=>"test_vagrant_box_image_1.1.1_0.img", + :virtual_size=>5 + }, + { + :name=>"test_vagrant_box_image_1.1.1_1.img", + :virtual_size=>10 + }, + { + :name=>"test_vagrant_box_image_1.1.1_2.img", + :virtual_size=>20 + } + ] + end + + it 'should create three disks in storage' do + expect(ui).to receive(:info).with('Creating image (snapshot of base box volume).') + expect(logger).to receive(:debug).with('Using pool default for base box snapshot') + expect(volumes).to receive(:create).with( + :xml => read_test_file('three_disks_in_storage_disk_0.xml'), + :pool_name => "default" + ) + expect(logger).to receive(:debug).with('Using pool default for base box snapshot') + expect(volumes).to receive(:create).with( + :xml => read_test_file('three_disks_in_storage_disk_1.xml'), + :pool_name => "default" + ) + expect(logger).to receive(:debug).with('Using pool default for base box snapshot') + expect(volumes).to receive(:create).with( + :xml => read_test_file('three_disks_in_storage_disk_2.xml'), + :pool_name => "default" + ) + expect(subject.call(env)).to be_nil + end + end + end +end diff --git a/spec/unit/action/create_domain_volume_spec/one_disk_in_storage.xml b/spec/unit/action/create_domain_volume_spec/one_disk_in_storage.xml new file mode 100644 index 0000000..f66c343 --- /dev/null +++ b/spec/unit/action/create_domain_volume_spec/one_disk_in_storage.xml @@ -0,0 +1,21 @@ + + test.img + 5 + + + + 0 + 0 + + + + + /test/path_0.img + + + 0 + 0 + + + + diff --git a/spec/unit/action/create_domain_volume_spec/three_disks_in_storage_disk_0.xml b/spec/unit/action/create_domain_volume_spec/three_disks_in_storage_disk_0.xml new file mode 100644 index 0000000..f66c343 --- /dev/null +++ b/spec/unit/action/create_domain_volume_spec/three_disks_in_storage_disk_0.xml @@ -0,0 +1,21 @@ + + test.img + 5 + + + + 0 + 0 + + + + + /test/path_0.img + + + 0 + 0 + + + + diff --git a/spec/unit/action/create_domain_volume_spec/three_disks_in_storage_disk_1.xml b/spec/unit/action/create_domain_volume_spec/three_disks_in_storage_disk_1.xml new file mode 100644 index 0000000..ecd4dc3 --- /dev/null +++ b/spec/unit/action/create_domain_volume_spec/three_disks_in_storage_disk_1.xml @@ -0,0 +1,21 @@ + + test_1.img + 10 + + + + 0 + 0 + + + + + /test/path_1.img + + + 0 + 0 + + + + diff --git a/spec/unit/action/create_domain_volume_spec/three_disks_in_storage_disk_2.xml b/spec/unit/action/create_domain_volume_spec/three_disks_in_storage_disk_2.xml new file mode 100644 index 0000000..1f39376 --- /dev/null +++ b/spec/unit/action/create_domain_volume_spec/three_disks_in_storage_disk_2.xml @@ -0,0 +1,21 @@ + + test_2.img + 20 + + + + 0 + 0 + + + + + /test/path_2.img + + + 0 + 0 + + + + diff --git a/spec/unit/action/handle_box_image_spec.rb b/spec/unit/action/handle_box_image_spec.rb new file mode 100644 index 0000000..2091934 --- /dev/null +++ b/spec/unit/action/handle_box_image_spec.rb @@ -0,0 +1,296 @@ +require 'spec_helper' +require 'support/sharedcontext' +require 'support/libvirt_context' + +require 'vagrant-libvirt/action/destroy_domain' + +describe VagrantPlugins::ProviderLibvirt::Action::HandleBoxImage do + subject { described_class.new(app, env) } + + include_context 'unit' + include_context 'libvirt' + + let(:libvirt_client) { double('libvirt_client') } + let(:volumes) { double('volumes') } + let(:all) { double('all') } + let(:box_volume) { double('box_volume') } + let(:fog_volume) { double('fog_volume') } + + describe '#call' do + before do + allow_any_instance_of(VagrantPlugins::ProviderLibvirt::Driver) + .to receive(:connection).and_return(connection) + allow(connection).to receive(:client).and_return(libvirt_client) + allow(connection).to receive(:volumes).and_return(volumes) + allow(volumes).to receive(:all).and_return(all) + allow(env[:ui]).to receive(:clear_line) + + end + + context 'when one disk in metadata.json' do + before do + allow(all).to receive(:first).and_return(box_volume) + allow(box_volume).to receive(:id).and_return(1) + allow(env[:machine]).to receive_message_chain("box.name") { 'test' } + allow(env[:machine]).to receive_message_chain("box.version") { '1.1.1' } + allow(env[:machine]).to receive_message_chain("box.metadata") { Hash[ + 'virtual_size'=> 5, + 'format' => 'qcow2' + ] + } + allow(env[:machine]).to receive_message_chain("box.directory.join") do |arg| + '/test/'.concat(arg.to_s) + end + end + + it 'should have one disk in machine env' do + expect(subject.call(env)).to be_nil + expect(env[:box_volume_number]).to eq(1) + expect(env[:box_volumes]).to eq( + [ + { + :path=>"/test/box.img", + :name=>"test_vagrant_box_image_1.1.1_0.img", + :virtual_size=>5, + :format=>"qcow2" + } + ] + ) + end + + context 'when disk image not in storage pool' do + before do + allow(File).to receive(:exist?).and_return(true) + allow(File).to receive(:size).and_return(5*1024*1024*1024) + allow(all).to receive(:first).and_return(nil) + allow(subject).to receive(:upload_image).and_return(true) + allow(volumes).to receive(:create).and_return(fog_volume) + end + + it 'should upload disk' do + expect(ui).to receive(:info).with('Uploading base box image as volume into Libvirt storage...') + expect(logger).to receive(:info).with('Creating volume test_vagrant_box_image_1.1.1_0.img in storage pool default.') + expect(volumes).to receive(:create).with( + hash_including( + :name => "test_vagrant_box_image_1.1.1_0.img", + :allocation => "5120M", + :capacity => "5G", + ) + ) + expect(subject).to receive(:upload_image) + expect(subject.call(env)).to be_nil + end + end + + context 'when disk image already in storage pool' do + before do + allow(all).to receive(:first).and_return(box_volume) + allow(box_volume).to receive(:id).and_return(1) + end + + it 'should skip disk upload' do + expect(volumes).not_to receive(:create) + expect(subject).not_to receive(:upload_image) + expect(subject.call(env)).to be_nil + end + end + end + + context 'when three disks in metadata.json' do + before do + allow(all).to receive(:first).and_return(box_volume) + allow(box_volume).to receive(:id).and_return(1) + allow(env[:machine]).to receive_message_chain("box.name") { 'test' } + allow(env[:machine]).to receive_message_chain("box.version") { '1.1.1' } + allow(env[:machine]).to receive_message_chain("box.metadata") { Hash[ + 'disks' => [ + { + 'name'=>'send_box_name.img', + 'virtual_size'=> 5, + }, + { + 'path' => 'disk.qcow2', + 'virtual_size'=> 10 + }, + { + 'virtual_size'=> 20, + }, + ], + 'format' => 'qcow2' + ] + } + allow(env[:machine]).to receive_message_chain("box.directory.join") do |arg| + '/test/'.concat(arg.to_s) + end + end + + it 'should have three disks in machine env' do + expect(subject.call(env)).to be_nil + expect(env[:box_volume_number]).to eq(3) + expect(env[:box_volumes]).to eq( + [ + { + :path=>"/test/box.img", + :name=>"send_box_name.img", + :virtual_size=>5, + :format=>"qcow2" + }, + { + :path=>"/test/disk.qcow2", + :name=>"test_vagrant_box_image_1.1.1_1.img", + :virtual_size=>10, + :format=>"qcow2" + }, + { + :path=>"/test/box_2.img", + :name=>"test_vagrant_box_image_1.1.1_2.img", + :virtual_size=>20, + :format=>"qcow2" + } + ] + ) + end + + context 'when none of the disks in storage pool' do + before do + allow(File).to receive(:exist?).and_return(true) + allow(File).to receive(:size).and_return(5*1024*1024*1024, 10*1024*1024*1024, 20*1024*1024*1024) + allow(all).to receive(:first).and_return(nil) + allow(subject).to receive(:upload_image).and_return(true) + allow(volumes).to receive(:create).and_return(fog_volume) + end + + it 'should upload all 3 disks' do + expect(ui).to receive(:info).with('Uploading base box image as volume into Libvirt storage...') + expect(logger).to receive(:info).with('Creating volume send_box_name.img in storage pool default.') + expect(volumes).to receive(:create).with( + hash_including( + :name => "send_box_name.img", + :allocation => "5120M", + :capacity => "5G", + ) + ) + expect(subject).to receive(:upload_image) + expect(ui).to receive(:info).with('Uploading base box image as volume into Libvirt storage...') + expect(logger).to receive(:info).with('Creating volume test_vagrant_box_image_1.1.1_1.img in storage pool default.') + expect(volumes).to receive(:create).with( + hash_including( + :name => "test_vagrant_box_image_1.1.1_1.img", + :allocation => "10240M", + :capacity => "10G", + ) + ) + expect(subject).to receive(:upload_image) + expect(ui).to receive(:info).with('Uploading base box image as volume into Libvirt storage...') + expect(logger).to receive(:info).with('Creating volume test_vagrant_box_image_1.1.1_2.img in storage pool default.') + expect(volumes).to receive(:create).with( + hash_including( + :name => "test_vagrant_box_image_1.1.1_2.img", + :allocation => "20480M", + :capacity => "20G", + ) + ) + expect(subject).to receive(:upload_image) + + expect(subject.call(env)).to be_nil + end + end + + context 'when only disk 0 in storage pool' do + before do + allow(File).to receive(:exist?).and_return(true) + allow(File).to receive(:size).and_return(10*1024*1024*1024, 20*1024*1024*1024) + allow(all).to receive(:first).and_return(box_volume, nil, nil) + allow(box_volume).to receive(:id).and_return(1) + allow(subject).to receive(:upload_image).and_return(true) + allow(volumes).to receive(:create).and_return(fog_volume) + end + + it 'upload disks 1 and 2 only' do + expect(ui).to receive(:info).with('Uploading base box image as volume into Libvirt storage...') + expect(logger).to receive(:info).with('Creating volume test_vagrant_box_image_1.1.1_1.img in storage pool default.') + expect(volumes).to receive(:create).with(hash_including(:name => "test_vagrant_box_image_1.1.1_1.img")) + expect(subject).to receive(:upload_image) + expect(ui).to receive(:info).with('Uploading base box image as volume into Libvirt storage...') + expect(logger).to receive(:info).with('Creating volume test_vagrant_box_image_1.1.1_2.img in storage pool default.') + expect(volumes).to receive(:create).with(hash_including(:name => "test_vagrant_box_image_1.1.1_2.img")) + expect(subject).to receive(:upload_image) + + expect(subject.call(env)).to be_nil + end + end + + context 'when has all disks on storage pool' do + before do + allow(all).to receive(:first).and_return(box_volume) + allow(box_volume).to receive(:id).and_return(1) + end + + it 'should skip disk upload' do + expect(ui).not_to receive(:info).with('Uploading base box image as volume into Libvirt storage...') + expect(volumes).not_to receive(:create) + expect(subject).not_to receive(:upload_image) + expect(subject.call(env)).to be_nil + end + end + end + + context 'when wrong box format in metadata.json' do + before do + allow(all).to receive(:first).and_return(box_volume) + allow(box_volume).to receive(:id).and_return(1) + allow(env[:machine]).to receive_message_chain("box.name") { 'test' } + allow(env[:machine]).to receive_message_chain("box.version") { '1.1.1' } + allow(env[:machine]).to receive_message_chain("box.metadata") { Hash[ + 'virtual_size'=> 5, + 'format' => 'wrongFormat' + ] + } + allow(env[:machine]).to receive_message_chain("box.directory.join") do |arg| + '/test/'.concat(arg.to_s) + end + end + + it 'should raise WrongBoxFormatSet exception' do + expect{ subject.call(env) }.to raise_error(VagrantPlugins::ProviderLibvirt::Errors::WrongBoxFormatSet) + end + + end + + context 'when one of a multi disk definition has wrong disk format in metadata.json' do + before do + allow(all).to receive(:first).and_return(box_volume) + allow(box_volume).to receive(:id).and_return(1) + allow(env[:machine]).to receive_message_chain("box.name") { 'test' } + allow(env[:machine]).to receive_message_chain("box.version") { '1.1.1' } + allow(env[:machine]).to receive_message_chain("box.metadata") { + Hash[ + 'disks' => [ + { + 'name'=>'send_box_name.img', + 'virtual_size'=> 5, + 'format'=> 'wrongFormat' + }, + { + 'path' => 'disk.qcow2', + 'virtual_size'=> 10 + }, + { + 'virtual_size'=> 20, + }, + ], + 'format' => 'qcow2', + ] + } + allow(env[:machine]).to receive_message_chain("box.directory.join") do |arg| + '/test/'.concat(arg.to_s) + end + end + + it 'should raise WrongDiskFormatSet exception' do + expect { subject.call(env) }.to raise_error(VagrantPlugins::ProviderLibvirt::Errors::WrongDiskFormatSet) + end + end + + end +end diff --git a/spec/unit/templates/domain_all_settings.xml b/spec/unit/templates/domain_all_settings.xml index fd52809..69eb0a6 100644 --- a/spec/unit/templates/domain_all_settings.xml +++ b/spec/unit/templates/domain_all_settings.xml @@ -47,6 +47,11 @@ + + + + + diff --git a/spec/unit/templates/domain_spec.rb b/spec/unit/templates/domain_spec.rb index 87b4578..51aaf61 100644 --- a/spec/unit/templates/domain_spec.rb +++ b/spec/unit/templates/domain_spec.rb @@ -9,6 +9,13 @@ describe 'templates/domain' do class DomainTemplateHelper < VagrantPlugins::ProviderLibvirt::Config include VagrantPlugins::ProviderLibvirt::Util::ErbTemplate + attr_accessor :domain_volumes + + def initialize + super + @domain_volumes = [] + end + def finalize! super end @@ -44,11 +51,22 @@ describe 'templates/domain' do domain.boot('cdrom') domain.boot('hd') domain.emulator_path = '/usr/bin/kvm-spice' - domain.instance_variable_set('@domain_volume_path', '/var/lib/libvirt/images/test.qcow2') domain.instance_variable_set('@domain_volume_cache', 'deprecated') domain.disk_bus = 'ide' domain.disk_device = 'vda' domain.disk_driver(:cache => 'unsafe', :io => 'threads', :copy_on_read => 'on', :discard => 'unmap', :detect_zeroes => 'on') + domain.domain_volumes.push({ + :dev => 1.vdev.to_s, + :cache => 'unsafe', + :bus => domain.disk_bus, + :path => '/var/lib/libvirt/images/test.qcow2' + }) + domain.domain_volumes.push({ + :dev => 2.vdev.to_s, + :cache => 'unsafe', + :bus => domain.disk_bus, + :path => '/var/lib/libvirt/images/test2.qcow2' + }) domain.storage(:file, path: 'test-disk1.qcow2') domain.storage(:file, path: 'test-disk2.qcow2', io: 'threads', copy_on_read: 'on', discard: 'unmap', detect_zeroes: 'on') domain.disks.each do |disk| diff --git a/tests/runtests.bats b/tests/runtests.bats index 0532b2e..fd355e5 100644 --- a/tests/runtests.bats +++ b/tests/runtests.bats @@ -6,6 +6,7 @@ VAGRANT_OPT="--provider=libvirt" TEMPDIR= + setup_file() { # set VAGRANT_HOME to something else to reuse for tests to avoid clashes with # user installed plugins when running tests locally. @@ -77,6 +78,19 @@ cleanup() { cleanup } +@test "bring up with two disks" { + export VAGRANT_CWD=tests/two_disks + cleanup + run ${VAGRANT_CMD} up ${VAGRANT_OPT} + echo "${output}" + echo "status = ${status}" + [ "$status" -eq 0 ] + echo "${output}" + [ $(expr "$output" : ".*Image.*2G") -ne 0 ] + [ $(expr "$output" : ".*Image.*10G") -ne 0 ] + cleanup +} + @test "bring up with adjusted memory settings" { export VAGRANT_CWD=tests/memory cleanup diff --git a/tests/two_disks/Vagrantfile b/tests/two_disks/Vagrantfile new file mode 100644 index 0000000..8b2a36a --- /dev/null +++ b/tests/two_disks/Vagrantfile @@ -0,0 +1,10 @@ +# -*- mode: ruby -*- +# vi: set ft=ruby : + +Vagrant.configure("2") do |config| + config.vm.box = "yamatoRT/tinycore-two-disks" + config.vm.box_version = "0.0.2" + config.ssh.shell = "/bin/sh" + config.vm.synced_folder ".", "/vagrant", disabled: true + +end diff --git a/tools/create_box_with_two_disks.sh b/tools/create_box_with_two_disks.sh new file mode 100755 index 0000000..149a550 --- /dev/null +++ b/tools/create_box_with_two_disks.sh @@ -0,0 +1,32 @@ + +VAGRANT_HOME=${1:-$HOME/.vagrant.d/} +VAGRANT_CMD=${2:-vagrant} + +echo 'Create box with two disks' +${VAGRANT_CMD} box list +if [ "$(${VAGRANT_CMD} box list | grep -c -E '^infernix/tinycore-two-disks\s')" -eq 0 ] +then + if [ "$(${VAGRANT_CMD} box list | grep -c -E '^infernix/tinycore\s')" -eq 0 ] + then + ${VAGRANT_CMD} box add infernix/tinycore + fi + NEW_PATH="${VAGRANT_HOME}/boxes/infernix-VAGRANTSLASH-tinycore-two-disks" + cp -r "${VAGRANT_HOME}/boxes/infernix-VAGRANTSLASH-tinycore" "${NEW_PATH}" + BOX_VERSION="$(${VAGRANT_CMD} box list --machine-readable | grep -A 10 infernix/tinycore-two-disks | grep box-version | cut -d, -f4)" + qemu-img create -f qcow2 "${NEW_PATH}/${BOX_VERSION}/libvirt/disk2.qcow2" 10G + cat > "${NEW_PATH}/${BOX_VERSION}/libvirt/metadata.json" <