Restore handling of disk_device domain setting (#1365)

Re-enable handling of the disk_device domain volume setting to ensure it
can be overridden from the default of vda to a value chosen.

Provide a disk resolver to resolve devices after the box has been downloaded
so that initial devices can be correctly allocated and avoid conflicts with
additional disks added that would otherwise get assigned the same device.

Removes hack for destroy domain when more than one disk, as now devices
in the config are only present if provided by the configuration.

Fixes: #1353
This commit is contained in:
Darragh Bailey 2021-11-22 10:02:18 +00:00 committed by GitHub
parent f8eff3d7a9
commit 91401a6559
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 420 additions and 54 deletions

View File

@ -2,6 +2,8 @@
require 'log4r'
require 'vagrant-libvirt/util/resolvers'
module VagrantPlugins
module ProviderLibvirt
module Action
@ -140,6 +142,8 @@ module VagrantPlugins
@os_type = 'hvm'
resolver = ::VagrantPlugins::ProviderLibvirt::Util::DiskDeviceResolver.new(prefix=@disk_device[0..1])
# Get path to domain image from the storage pool selected if we have a box.
if env[:machine].config.vm.box
if @snapshot_pool_name != @storage_pool_name
@ -147,6 +151,12 @@ module VagrantPlugins
else
pool_name = @storage_pool_name
end
# special handling for domain volume
env[:box_volumes][0][:device] = env[:box_volumes][0].fetch(:device, @disk_device)
resolver.resolve!(env[:box_volumes])
@logger.debug "Search for volumes in pool: #{pool_name}"
env[:box_volumes].each_index do |index|
suffix_index = index > 0 ? "_#{index}" : ''
@ -154,14 +164,16 @@ module VagrantPlugins
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,
:dev => env[:box_volumes][index][:device],
:cache => @domain_volume_cache,
:bus => @disk_bus,
:path => domain_volume.path,
:virtual_size => env[:box_volumes][index][:virtual_size]
})
end
end
end
# If we have a box, take the path from the domain volume and set our storage_prefix.
@ -173,19 +185,7 @@ module VagrantPlugins
storage_prefix = get_disk_storage_prefix(env, @storage_pool_name)
end
@serials = config.serials
@serials.each do |serial|
next unless serial[:source] && serial[:source][:path]
dir = File.dirname(serial[:source][:path])
begin
FileUtils.mkdir_p(dir)
rescue ::Errno::EACCES
raise Errors::SerialCannotCreatePathError,
path: dir
end
end
resolver.resolve!(@disks)
@disks.each do |disk|
disk[:path] ||= _disk_name(@name, disk)
@ -235,6 +235,20 @@ module VagrantPlugins
end
end
@serials = config.serials
@serials.each do |serial|
next unless serial[:source] && serial[:source][:path]
dir = File.dirname(serial[:source][:path])
begin
FileUtils.mkdir_p(dir)
rescue ::Errno::EACCES
raise Errors::SerialCannotCreatePathError,
path: dir
end
end
# Output the settings we're going to use to the user
env[:ui].info(I18n.t('vagrant_libvirt.creating_domain'))
env[:ui].info(" -- Name: #{@name}")
@ -284,7 +298,7 @@ module VagrantPlugins
end
env[:ui].info(" -- Storage pool: #{@storage_pool_name}")
@domain_volumes.each do |volume|
env[:ui].info(" -- Image(#{volume[:device]}): #{volume[:path]}, #{volume[:virtual_size].to_GB}G")
env[:ui].info(" -- Image(#{volume[:dev]}): #{volume[:path]}, #{volume[:virtual_size].to_GB}G")
end
if not @disk_driver_opts.empty?

View File

@ -128,12 +128,7 @@ module VagrantPlugins
else
# otherwise fallback to find the disk by device if specified by user
# and finally index counting with offset and hope the match is correct
if offset > 1
# Currently disk devices are resolved incorrectly if the box has more than one device
# this should be solved subsequently by delaying the resolution, however for now
# default to using offset when more than one box volume.
domain_disk = disks_xml[offset + index]
elsif !disk[:device].nil?
if !disk[:device].nil?
domain_disk = REXML::XPath.match(disks_xml, './target[@dev="' + disk[:device] + '"]').first
domain_disk = domain_disk.parent if !domain_disk.nil?
else

View File

@ -4,15 +4,8 @@ require 'cgi'
require 'vagrant'
class Numeric
Alphabet = ('a'..'z').to_a
def vdev
s = String.new
q = self
(q, r = (q - 1).divmod(26)) && s.prepend(Alphabet[r]) until q.zero?
"vd#{s}"
end
end
require 'vagrant-libvirt/errors'
require 'vagrant-libvirt/util/resolvers'
module VagrantPlugins
module ProviderLibvirt
@ -353,17 +346,6 @@ module VagrantPlugins
@boot_order << device # append
end
def _get_device(disks)
# skip existing devices and also the first one (vda)
exist = disks.collect { |x| x[:device] } + [1.vdev.to_s]
skip = 1 # we're 1 based, not 0 based...
loop do
dev = skip.vdev # get lettered device
return dev unless exist.include?(dev)
skip += 1
end
end
def _get_cdrom_dev(cdroms)
exist = Hash[cdroms.collect { |x| [x[:dev], true] }]
# hda - hdc
@ -914,10 +896,6 @@ module VagrantPlugins
# Storage
@disks = [] if @disks == UNSET_VALUE
@disks.map! do |disk|
disk[:device] = _get_device(@disks) if disk[:device].nil?
disk
end
@cdroms = [] if @cdroms == UNSET_VALUE
@cdroms.map! do |cdrom|
cdrom[:dev] = _get_cdrom_dev(@cdroms) if cdrom[:dev].nil?
@ -998,7 +976,6 @@ module VagrantPlugins
end
end
machine.provider_config.disks.each do |disk|
if disk[:path] && (disk[:path][0] == '/')
errors << "absolute volume paths like '#{disk[:path]}' not yet supported"
@ -1011,6 +988,16 @@ module VagrantPlugins
end
end
# this won't be able to fully resolve the disks until the box has
# been downloaded and any devices that need to be assigned to the
# disks contained have been allocated
disk_resolver = ::VagrantPlugins::ProviderLibvirt::Util::DiskDeviceResolver.new
begin
disk_resolver.resolve(machine.provider_config.disks)
rescue Errors::VagrantLibvirtError => e
errors << "#{e}"
end
machine.config.vm.networks.each do |_type, opts|
if opts[:mac]
if opts[:mac] =~ /\A([0-9a-fA-F]{12})\z/

View File

@ -18,6 +18,14 @@ module VagrantPlugins
error_key(:package_not_supported)
end
class DuplicateDiskDevice < VagrantLibvirtError
error_key(:duplicate_disk_device)
end
class NoDiskDeviceAvailable < VagrantLibvirtError
error_key(:no_disk_device_available)
end
# Storage pools and volumes exceptions
class NoStoragePool < VagrantLibvirtError
error_key(:no_storage_pool)

View File

@ -0,0 +1,80 @@
# frozen_string_literal: true
require 'log4r'
require 'vagrant-libvirt/errors'
module VagrantPlugins
module ProviderLibvirt
module Util
class DiskDeviceResolver
attr_reader :existing
def initialize(prefix='vd')
@default_prefix = prefix
@device_indicies = Hash.new
@existing = Hash.new
end
def resolve!(disks, options={})
# check for duplicate device entries and raise an exception if one found
# with enough details that the user should be able to determine what
# to do to resolve.
disks.select { |x| !x[:device].nil? }.each do |x|
if @existing.has_key?(x[:device])
raise Errors::DuplicateDiskDevice, new_disk: x, existing_disk: @existing[x[:device]]
end
@existing[x[:device]] = x
end
disks.each_index do |index|
dev = disks[index][:device]
if dev.nil?
prefix = options.fetch(:prefix, @default_prefix)
dev = next_device(prefix=prefix)
if dev.nil?
raise Errors::NoDiskDeviceAvailable, prefix: prefix
end
disks[index][:device] = dev
@existing[dev] = disks[index]
end
end
end
def resolve(disks)
new_disks = []
disks.each do |disk|
new_disks.push disk.dup
end
resolve!(new_disks)
new_disks
end
private
def next_device(prefix)
curr = device_index(prefix)
while curr <= 'z'.ord
dev = prefix + curr.chr
if !@existing[dev].nil?
curr += 1
next
else
@device_indicies[prefix] = curr
return dev
end
end
end
def device_index(prefix)
@device_indicies[prefix] ||= 'a'.ord
end
end
end
end
end

View File

@ -81,6 +81,8 @@ en:
errors:
call_chain_error: Invalid action chain, must ensure that '%{require_action}' is called prior to calling '%{current_action}'
package_not_supported: No support for package with Libvirt. Create box manually.
duplicate_disk_device: Disk with details '%{new_disk}' duplicates :device attribute of disk '%{existing_disk}'
no_disk_device_available: All available devices allocated under '%{prefix}x', try a different type
fog_error: |-
There was an error talking to Libvirt. The error message is shown
below:

View File

@ -33,7 +33,6 @@ describe VagrantPlugins::ProviderLibvirt::Action::CreateDomain do
allow(connection).to receive(:volumes).and_return(volumes)
allow(volumes).to receive(:all).and_return([domain_volume])
allow(domain_volume).to receive(:pool_name).and_return('default')
allow(domain_volume).to receive(:[]).with('name').and_return('vagrant-test_default.img')
allow(domain_volume).to receive(:path).and_return('/var/lib/libvirt/images/vagrant-test_default.img')
allow(machine).to receive_message_chain("box.name") { 'vagrant-libvirt/test' }
@ -117,6 +116,48 @@ describe VagrantPlugins::ProviderLibvirt::Action::CreateDomain do
end
end
end
context 'with custom disk device setting' do
let(:domain_xml_file) { 'custom_disk_settings.xml' }
let(:vagrantfile_providerconfig) {
<<-EOF
libvirt.disk_device = 'sda'
EOF
}
it 'should set the domain device' do
expect(ui).to receive(:info).with(/ -- Image\(sda\):.*/)
expect(servers).to receive(:create).with(xml: domain_xml).and_return(machine)
expect(subject.call(env)).to be_nil
end
end
context 'with two domain disks' do
let(:domain_xml_file) { 'two_disk_settings.xml' }
let(:domain_volume_2) { double('domain_volume 2') }
before do
expect(volumes).to receive(:all).and_return([domain_volume])
expect(volumes).to receive(:all).and_return([domain_volume_2])
expect(domain_volume_2).to receive(:pool_name).and_return('default')
expect(domain_volume_2).to receive(:path).and_return('/var/lib/libvirt/images/vagrant-test_default_1.img')
env[:box_volumes].push({
:path=>"/test/box_1.img",
:name=>"test_vagrant_box_image_1.1.1_1.img",
:virtual_size=> ByteNumber.new(5),
})
end
it 'should correctly assign device entries' do
expect(ui).to receive(:info).with(/ -- Image\(vda\):.*/)
expect(ui).to receive(:info).with(/ -- Image\(vdb\):.*/)
expect(servers).to receive(:create).with(xml: domain_xml).and_return(machine)
expect(subject.call(env)).to be_nil
end
end
end
context 'no default pool' do

View File

@ -0,0 +1,55 @@
<domain type='kvm' xmlns:qemu='http://libvirt.org/schemas/domain/qemu/1.0'>
<name>vagrant-test_default</name>
<title></title>
<description>Source: /rootpath/Vagrantfile</description>
<uuid></uuid>
<memory>524288</memory>
<vcpu>1</vcpu>
<cpu mode='host-model'>
<model fallback='allow'></model>
</cpu>
<os>
<type>hvm</type>
<kernel></kernel>
<initrd></initrd>
<cmdline></cmdline>
</os>
<features>
<acpi/>
<apic/>
<pae/>
</features>
<clock offset='utc'>
</clock>
<devices>
<disk type='file' device='disk'>
<alias name='ua-box-volume-0'/>
<driver name='qemu' type='qcow2' cache='default'/>
<source file='/var/lib/libvirt/images/vagrant-test_default.img'/>
<target dev='sda' bus='virtio'/>
</disk>
<serial type='pty'>
<target port='0'/>
</serial>
<console type='pty'>
<target port='0'/>
</console>
<input type='mouse' bus='ps2'/>
<graphics type='vnc' port='-1' autoport='yes' listen='127.0.0.1' keymap='en-us' />
<video>
<model type='cirrus' vram='9216' heads='1'/>
</video>
</devices>
</domain>

View File

@ -0,0 +1,61 @@
<domain type='kvm' xmlns:qemu='http://libvirt.org/schemas/domain/qemu/1.0'>
<name>vagrant-test_default</name>
<title></title>
<description>Source: /rootpath/Vagrantfile</description>
<uuid></uuid>
<memory>524288</memory>
<vcpu>1</vcpu>
<cpu mode='host-model'>
<model fallback='allow'></model>
</cpu>
<os>
<type>hvm</type>
<kernel></kernel>
<initrd></initrd>
<cmdline></cmdline>
</os>
<features>
<acpi/>
<apic/>
<pae/>
</features>
<clock offset='utc'>
</clock>
<devices>
<disk type='file' device='disk'>
<alias name='ua-box-volume-0'/>
<driver name='qemu' type='qcow2' cache='default'/>
<source file='/var/lib/libvirt/images/vagrant-test_default.img'/>
<target dev='vda' bus='virtio'/>
</disk>
<disk type='file' device='disk'>
<alias name='ua-box-volume-1'/>
<driver name='qemu' type='qcow2' cache='default'/>
<source file='/var/lib/libvirt/images/vagrant-test_default_1.img'/>
<target dev='vdb' bus='virtio'/>
</disk>
<serial type='pty'>
<target port='0'/>
</serial>
<console type='pty'>
<target port='0'/>
</console>
<input type='mouse' bus='ps2'/>
<graphics type='vnc' port='-1' autoport='yes' listen='127.0.0.1' keymap='en-us' />
<video>
<model type='cirrus' vram='9216' heads='1'/>
</video>
</devices>
</domain>

View File

@ -606,11 +606,11 @@ describe VagrantPlugins::ProviderLibvirt::Config do
end
context 'without devices given' do
it 'should merge disks with different devices assigned automatically' do
it 'should merge disks without assigning devices automatically' do
one.storage(:file)
two.storage(:file)
subject.finalize!
expect(subject.disks).to include(include(device: 'vdb'),
expect(subject.disks).to_not include(include(device: 'vdb'),
include(device: 'vdc'))
end
end

View File

@ -59,13 +59,13 @@
<alias name='ua-disk-volume-0'/>
<driver name='qemu' type='qcow2' cache='default'/>
<source file='/var/lib/libvirt/images/test-disk1.qcow2'/>
<target dev='vdb' bus='virtio'/>
<target dev='vdc' bus='virtio'/>
</disk>
<disk type='file' device='disk'>
<alias name='ua-disk-volume-1'/>
<driver name='qemu' type='qcow2' cache='default' io='threads' copy_on_read='on' discard='unmap' detect_zeroes='on'/>
<source file='/var/lib/libvirt/images/test-disk2.qcow2'/>
<target dev='vdc' bus='virtio'/>
<target dev='vdd' bus='virtio'/>
</disk>
<disk type='file' device='cdrom'>

View File

@ -59,13 +59,13 @@ describe 'templates/domain' do
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,
:dev => 'vda',
:cache => 'unsafe',
:bus => domain.disk_bus,
:path => '/var/lib/libvirt/images/test.qcow2'
})
domain.domain_volumes.push({
:dev => 2.vdev.to_s,
:dev => 'vdb',
:cache => 'unsafe',
:bus => domain.disk_bus,
:path => '/var/lib/libvirt/images/test2.qcow2'
@ -118,6 +118,13 @@ describe 'templates/domain' do
let(:test_file) { 'domain_all_settings.xml' }
it 'renders template' do
domain.finalize!
# resolving is now done during create domain, so need to recreate
# the same behaviour before calling the template until that
# is separated out from create domain.
resolver = ::VagrantPlugins::ProviderLibvirt::Util::DiskDeviceResolver.new(prefix=domain.disk_device[0..1])
resolver.resolve!(domain.domain_volumes.dup.each { |volume| volume[:device] = volume[:dev] })
resolver.resolve!(domain.disks)
expect(domain.to_xml('domain')).to eq xml_expected
end
end

View File

@ -0,0 +1,116 @@
# frozen_string_literal: true
require 'spec_helper'
require 'vagrant-libvirt/util/resolvers'
describe VagrantPlugins::ProviderLibvirt::Util::DiskDeviceResolver do
subject { described_class.new }
def deep_clone_disks(disk_array)
new_array = []
disk_array.each do |disk|
new_array.push disk.dup
end
new_array
end
describe '#resolve!' do
context 'when using default prefix' do
[
[
[{:name => 'single-disk'}],
[{:name => 'single-disk', :device => 'vda'}],
],
[
[{:name => 'disk1'}, {:name => 'disk2'}],
[{:name => 'disk1', :device => 'vda'}, {:name => 'disk2', :device => 'vdb'}],
],
[
[{:name => 'disk1'}, {:name => 'disk2', :device => 'vdc'}],
[{:name => 'disk1', :device => 'vda'}, {:name => 'disk2', :device => 'vdc'}],
],
[
[{:name => 'disk1', :device => 'sda'}, {:name => 'disk2'}],
[{:name => 'disk1', :device => 'sda'}, {:name => 'disk2', :device => 'vda'}],
],
].each do |input_disks, output_disks, options={}|
opts = {}.merge!(options)
it "should handle inputs: #{input_disks}" do
disks = deep_clone_disks(input_disks)
expect(subject.resolve!(disks)).to eq(output_disks)
expect(disks).to_not eq(input_disks)
end
end
end
context 'when using different default prefix' do
let(:subject) { described_class.new('sd') }
[
[
[{:name => 'single-disk'}],
[{:name => 'single-disk', :device => 'sda'}],
],
[
[{:name => 'disk1'}, {:name => 'disk2'}],
[{:name => 'disk1', :device => 'sda'}, {:name => 'disk2', :device => 'sdb'}],
],
[
[{:name => 'disk1'}, {:name => 'disk2', :device => 'vdc'}],
[{:name => 'disk1', :device => 'sda'}, {:name => 'disk2', :device => 'vdc'}],
],
[
[{:name => 'disk1', :device => 'sda'}, {:name => 'disk2'}],
[{:name => 'disk1', :device => 'sda'}, {:name => 'disk2', :device => 'sdb'}],
],
[
[{:name => 'disk1'}, {:name => 'disk2', :device => 'sda'}],
[{:name => 'disk1', :device => 'sdb'}, {:name => 'disk2', :device => 'sda'}],
],
].each do |input_disks, output_disks, options={}|
opts = {}.merge!(options)
it "should handle inputs: #{input_disks}" do
disks = deep_clone_disks(input_disks)
expect(subject.resolve!(disks)).to eq(output_disks)
end
end
end
context 'when using custom prefix' do
[
[
[{:name => 'existing-disk', :device => 'vda'}],
[{:name => 'single-disk'}],
[{:name => 'single-disk', :device => 'sda'}],
{:prefix => 'sd'},
],
[
[{:name => 'existing-disk', :device => 'vda'}],
[{:name => 'disk1', :device => 'sda'}, {:name => 'disk2'}],
[{:name => 'disk1', :device => 'sda'}, {:name => 'disk2', :device => 'sdb'}],
{:prefix => 'sd'},
],
].each do |existing, input_disks, output_disks, options={}|
opts = {}.merge!(options)
it "should handle inputs: #{input_disks} with opts: #{opts}" do
disks = deep_clone_disks(input_disks)
subject.resolve(existing)
expect(subject.resolve!(disks, opts)).to eq(output_disks)
end
end
end
end
describe '#resolve' do
let(:input_disks) { [{:name => 'single-disk'}] }
let(:output_disks) { [{:name => 'single-disk', :device => 'vda'}] }
it "should resolve without modifying" do
disks = deep_clone_disks(input_disks)
expect(subject.resolve(disks)).to eq(output_disks)
expect(disks).to_not eq(output_disks)
expect(disks).to eq(input_disks)
end
end
end