Files
vagrant-libvirt/spec/unit/driver_spec.rb
Darragh Bailey 4fe53477b2 Support running tests in parallel (#1667)
To allow for faster execution of the unit tests locally, allow running
them in parallel.
2022-11-08 16:28:32 +00:00

439 lines
15 KiB
Ruby

# frozen_string_literal: true
require_relative '../spec_helper'
require_relative '../support/binding_proc'
require 'fog/libvirt/requests/compute/dhcp_leases'
require 'vagrant-libvirt/driver'
describe VagrantPlugins::ProviderLibvirt::Driver do
include_context 'unit'
include_context 'libvirt'
subject { described_class.new(machine) }
# make it easier for distros that want to switch the default value for
# qemu_use_session to true by ensuring it is explicitly false for tests.
before do
allow(machine.provider_config).to receive(:qemu_use_session).and_return(false)
allow(logger).to receive(:info)
allow(logger).to receive(:debug)
end
describe 'connections' do
let(:vagrantfile) do
<<-EOF
Vagrant.configure('2') do |config|
config.vm.define :test1 do |node|
node.vm.provider :libvirt do |domain|
domain.uri = "qemu+ssh://user@remote1/system"
end
end
config.vm.define :test2 do |node|
node.vm.provider :libvirt do |domain|
domain.uri = "qemu+ssh://vms@remote2/system"
end
end
end
EOF
end
# need to override the default package iso_env as using a different
# name for the test machines above.
let(:machine) { iso_env.machine(:test1, :libvirt) }
let(:machine2) { iso_env.machine(:test2, :libvirt) }
let(:connection1) { double("connection 1") }
let(:connection2) { double("connection 2") }
let(:system_connection1) { double("system connection 1") }
let(:system_connection2) { double("system connection 2") }
# make it easier for distros that want to switch the default value for
# qemu_use_session to true by ensuring it is explicitly false for tests.
before do
allow(machine.provider).to receive('driver').and_call_original
allow(machine2.provider).to receive('driver').and_call_original
end
describe '#connection' do
it 'should configure a separate connection per machine' do
expect(Fog::Compute).to receive(:new).with(
hash_including({:libvirt_uri => 'qemu+ssh://user@remote1/system'})).and_return(connection1)
expect(Fog::Compute).to receive(:new).with(
hash_including({:libvirt_uri => 'qemu+ssh://vms@remote2/system'})).and_return(connection2)
expect(machine.provider.driver.connection).to eq(connection1)
expect(machine2.provider.driver.connection).to eq(connection2)
end
it 'should configure the connection once' do
expect(Fog::Compute).to receive(:new).once().and_return(connection1)
expect(machine.provider.driver.connection).to eq(connection1)
expect(machine.provider.driver.connection).to eq(connection1)
expect(machine.provider.driver.connection).to eq(connection1)
end
end
describe '#system_connection' do
# note that the urls for the two tests are currently
# incorrect here as they should be the following:
# qemu+ssh://user@remote1/system
# qemu+ssh://vms@remote2/system
#
# In that the system uri should be resolved based on
# the provider uri so that for:
# uri => qemu+ssh://user@remote1/session
# system_uri should be 'qemu+ssh://user@remote1/system'
# and not 'qemu:///system'.
it 'should configure a separate connection per machine' do
expect(Libvirt).to receive(:open_read_only).with('qemu+ssh://user@remote1/system').and_return(system_connection1)
expect(Libvirt).to receive(:open_read_only).with('qemu+ssh://vms@remote2/system').and_return(system_connection2)
expect(machine.provider.driver.system_connection).to eq(system_connection1)
expect(machine2.provider.driver.system_connection).to eq(system_connection2)
end
it 'should configure the connection once' do
expect(Libvirt).to receive(:open_read_only).with('qemu+ssh://user@remote1/system').and_return(system_connection1)
expect(machine.provider.driver.system_connection).to eq(system_connection1)
expect(machine.provider.driver.system_connection).to eq(system_connection1)
expect(machine.provider.driver.system_connection).to eq(system_connection1)
end
end
end
describe '#get_ipaddress' do
context 'when domain exists' do
# not used yet, but this is the form that is returned from addresses
let(:addresses) { {
:public => ["192.168.122.111"],
:private => ["192.168.122.111"],
} }
before do
allow(subject).to receive(:get_domain).and_return(domain)
end
it 'should retrieve the address via domain fog-libvirt API' do
# ideally should be able to yield a block to wait_for and check that
# the 'addresses' function on the domain is called correctly.
expect(domain).to receive(:wait_for).and_return(nil)
expect(subject.get_ipaddress).to eq(nil)
end
context 'when qemu_use_agent is enabled' do
let(:qemu_agent_interfaces) {
<<-EOF
{
"return": [
{
"name": "lo",
"ip-addresses": [
{
"ip-address-type": "ipv4",
"ip-address": "127.0.0.1",
"prefix": 8
}
],
"hardware-address": "00:00:00:00:00:00"
},
{
"name": "eth0",
"ip-addresses": [
{
"ip-address-type": "ipv4",
"ip-address": "192.168.122.42",
"prefix": 24
}
],
"hardware-address": "52:54:00:f8:67:98"
}
]
}
EOF
}
before do
allow(machine.provider_config).to receive(:qemu_use_agent).and_return(true)
end
it 'should retrieve the address via the agent' do
expect(subject).to receive(:connection).and_return(connection)
expect(libvirt_client).to receive(:lookup_domain_by_uuid).and_return(libvirt_domain)
expect(libvirt_domain).to receive(:qemu_agent_command).and_return(qemu_agent_interfaces)
expect(domain).to receive(:mac).and_return("52:54:00:f8:67:98").exactly(2).times
expect(subject.get_ipaddress).to eq("192.168.122.42")
end
context 'when qemu_use_session is enabled' do
before do
allow(machine.provider_config).to receive(:qemu_use_session).and_return(true)
end
it 'should still retrieve the address via the agent' do
expect(subject).to receive(:connection).and_return(connection)
expect(libvirt_client).to receive(:lookup_domain_by_uuid).and_return(libvirt_domain)
expect(libvirt_domain).to receive(:qemu_agent_command).and_return(qemu_agent_interfaces)
expect(domain).to receive(:mac).and_return("52:54:00:f8:67:98").exactly(2).times
expect(subject.get_ipaddress).to eq("192.168.122.42")
end
end
end
context 'when qemu_use_session is enabled' do
let(:system_connection) { double("system connection") }
let(:networks) { [instance_double(::Libvirt::Network)] }
let(:dhcp_leases) {
{
"iface" =>"virbr0",
"expirytime" =>1636287162,
"type" =>0,
"mac" =>"52:54:00:8b:dc:5f",
"ipaddr" =>"192.168.122.43",
"prefix" =>24,
"hostname" =>"vagrant-default_test",
"clientid" =>"ff:00:8b:dc:5f:00:01:00:01:29:1a:65:42:52:54:00:8b:dc:5f",
}
}
before do
allow(machine.provider_config).to receive(:qemu_use_session).and_return(true)
end
it 'should retrieve the address via the system dhcp-leases API' do
expect(domain).to receive(:mac).and_return("52:54:00:8b:dc:5f")
expect(subject).to receive(:list_all_networks).and_return(networks)
expect(networks[0]).to receive(:dhcp_leases).and_return([dhcp_leases])
expect(subject.get_ipaddress).to eq("192.168.122.43")
end
context 'when qemu_use_agent is enabled' do
before do
allow(machine.provider_config).to receive(:qemu_use_agent).and_return(true)
end
it 'should retrieve the address via the agent' do
expect(subject).to receive(:get_ipaddress_from_qemu_agent).and_return("192.168.122.44")
expect(subject.get_ipaddress).to eq("192.168.122.44")
end
end
end
end
end
describe '#list_all_networks' do
let(:vagrantfile_providerconfig) do
<<-EOF
libvirt.uri = "qemu:///system"
EOF
end
let(:libvirt_networks) { [
instance_double(::Libvirt::Network),
instance_double(::Libvirt::Network),
instance_double(::Libvirt::Network),
] }
before do
allow(subject).to receive(:connection).and_return(connection)
allow(connection).to receive(:client).and_return(libvirt_client)
expect(libvirt_client).to receive(:list_all_networks).and_return(libvirt_networks)
end
it 'should list networks' do
expect(libvirt_networks[0]).to receive(:bridge_name).and_return('')
expect(libvirt_networks[1]).to receive(:bridge_name).and_return('virbr0')
expect(libvirt_networks[2]).to receive(:bridge_name).and_return('virbr1')
expect(subject.list_all_networks).to eq(libvirt_networks)
end
it 'should skip networks missing bridge_name' do
expect(libvirt_networks[0]).to receive(:bridge_name).and_return('')
expect(libvirt_networks[1]).to receive(:bridge_name).and_raise(Libvirt::Error)
expect(libvirt_networks[1]).to receive(:name).and_return('bad_network')
expect(libvirt_networks[2]).to receive(:bridge_name).and_return('virbr1')
expect(subject.list_all_networks).to eq([libvirt_networks[0], libvirt_networks[2]])
end
end
describe '#host_devices' do
let(:vagrantfile_providerconfig) do
<<-EOF
libvirt.uri = "qemu:///system"
EOF
end
let(:ip_link_show) {
JSON.dump(
[
# trimmed element details of what would be returned by 'ip -j link show'
{ "ifindex": 1, "ifname": "lo", "group": "default", "link_type": "loopback"},
{ "ifindex": 2, "ifname": "eth0", "group": "default", "link_type": "ether"},
{ "ifindex": 3, "ifname": "eth1", "group": "default", "link_type": "ether"},
{ "ifindex": 4, "ifname": "virbr0", "group": "default", "link_type": "ether"},
]
)
}
let(:libvirt_interfaces) { [
instance_double(Libvirt::Interface),
instance_double(Libvirt::Interface),
] }
let(:libvirt_networks) { [
instance_double(Libvirt::Network),
instance_double(Libvirt::Network),
] }
before do
allow(subject).to receive(:connection).and_return(connection)
allow(Vagrant::Util::Subprocess).to receive(:execute) do |*arr|
expect(arr[0]).to eq('ip')
end.and_return(Vagrant::Util::Subprocess::Result.new(exit_code=0, stdout=ip_link_show, stderr=''))
expect(libvirt_client).to receive(:list_all_interfaces).and_return(libvirt_interfaces)
expect(subject).to receive(:list_all_networks).and_return(libvirt_networks)
expect(libvirt_interfaces[0]).to receive(:name).and_return('eth0')
expect(libvirt_interfaces[1]).to receive(:name).and_return('virbr0')
expect(libvirt_networks[0]).to receive(:bridge_name).and_return('')
expect(libvirt_networks[1]).to receive(:bridge_name).and_return('virbr0')
end
it 'should query system and libvirt' do
expect(subject.host_devices).to eq(['lo', 'eth0', 'eth1', 'virbr0'])
end
it 'should handle empty string' do
expect(machine.provider_config).to receive(:proxy_command).and_return('').twice
expect(subject.host_devices).to eq(['lo', 'eth0', 'eth1', 'virbr0'])
end
it 'should cache the result' do
expect(machine.provider_config).to receive(:proxy_command).and_return(nil).once
expect(subject.host_devices).to eq(['lo', 'eth0', 'eth1', 'virbr0'])
expect(subject.host_devices).to eq(['lo', 'eth0', 'eth1', 'virbr0'])
end
context 'when libvirt is remote' do
let(:vagrantfile_providerconfig) do
<<-EOF
libvirt.uri = "qemu+ssh://remote-server/system"
EOF
end
before do
allow(machine.provider_config).to receive(:proxy_command).and_return('ssh remote-server -W %h:%p')
end
it 'should use ssh for ip link' do
expect(Vagrant::Util::Subprocess).to receive(:execute) do |*arr|
expect(arr[0..3]).to eq(['ssh', 'remote-server', 'ip', '-j'])
end.and_return(Vagrant::Util::Subprocess::Result.new(exit_code=0, stdout=ip_link_show, stderr=''))
expect(subject.host_devices).to eq(['lo', 'eth0', 'eth1', 'virbr0'])
end
it 'should construct the ssh command with all options when needed' do
machine.provider_config.port = 2022
machine.provider_config.username = 'remote-user'
machine.provider_config.id_ssh_key_file = 'my-key-file'
expect(Vagrant::Util::Subprocess).to receive(:execute) do |*arr|
expect(arr[0..9]).to eq(['ssh', 'remote-server', '-p', '2022', '-l', 'remote-user', '-i', 'my-key-file', 'ip', '-j'])
end.and_return(Vagrant::Util::Subprocess::Result.new(exit_code=0, stdout=ip_link_show, stderr=''))
expect(subject.host_devices).to eq(['lo', 'eth0', 'eth1', 'virbr0'])
end
end
end
describe '#state' do
let(:domain) { double('domain') }
before do
allow(subject).to receive(:get_domain).and_return(domain)
end
[
[
'not found',
:not_created,
{
:setup => ProcWithBinding.new do
expect(subject).to receive(:get_domain).and_return(nil)
end,
}
],
[
'libvirt error',
:not_created,
{
:setup => ProcWithBinding.new do
expect(subject).to receive(:get_domain).and_raise(Libvirt::RetrieveError, 'missing')
end,
}
],
[
nil,
:unknown,
{
:setup => ProcWithBinding.new do
expect(domain).to receive(:state).and_return('unknown').at_least(:once)
end,
}
],
[
'terminated',
:not_created,
{
:setup => ProcWithBinding.new do
expect(domain).to receive(:state).and_return('terminated').at_least(:once)
end,
}
],
[
'no IP returned',
:inaccessible,
{
:setup => ProcWithBinding.new do
expect(domain).to receive(:state).and_return('running').at_least(:once)
expect(subject).to receive(:get_ipaddress).and_raise(Fog::Errors::TimeoutError)
end,
}
],
[
'running',
:running,
{
:setup => ProcWithBinding.new do
expect(domain).to receive(:state).and_return('running').at_least(:once)
expect(subject).to receive(:get_ipaddress).and_return('192.168.121.2')
end,
}
],
].each do |name, expected, options|
opts = {}
opts.merge!(options) if options
it "should handle '#{name}' by returning '#{expected}'" do
if !opts[:setup].nil?
opts[:setup].apply_binding(binding)
end
expect(subject.state).to eq(expected)
end
end
end
end