Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 54 additions & 4 deletions lib/fog/libvirt/models/compute/server.rb
Original file line number Diff line number Diff line change
Expand Up @@ -259,11 +259,14 @@
user_data_path = File.join(dir_path, 'user-data')
File.write(user_data_path, user_data)

isofile = Tempfile.new(['init', '.iso']).path
iso_tempfile = Tempfile.new(['init', '.iso'])
isofile = iso_tempfile.path
unless system('xorrisofs', '-output', isofile, '-volid', 'cidata', '-joliet', '-rock', user_data_path, meta_data_path)
raise Fog::Errors::Error.new("Couldn't generate cloud-init iso disk with xorrisofs.")
end
blk.call(isofile)
ensure
iso_tempfile&.close!
end

def create_user_data_iso
Expand Down Expand Up @@ -470,6 +473,44 @@
args
end

DNSMASQ_LEASE_DIR = '/var/lib/libvirt/dnsmasq'.freeze

# Read IP from a dnsmasq lease file when the libvirt DHCPLeases API
# returns nothing. This happens when DHCP is provided by an external
# dnsmasq (not started by libvirt) -- for example, a dnsmasq running
# inside a network namespace to work around port conflicts on WSL2.
#
# dnsmasq lease file format (space-separated):
# <expiry> <mac> <ip> <hostname> [<client-id>]
def ip_address_from_leasefile(net, mac)
net_name = net.name
return nil unless net_name

lease_file = File.join(DNSMASQ_LEASE_DIR, "#{net_name}.leases")
return nil unless File.exist?(lease_file)

target_mac = mac.to_s.downcase
best_expiry = 0
best_ip = nil

begin
File.foreach(lease_file) do |line|
parts = line.strip.split
next unless parts.length >= 4

expiry, lease_mac, ip = parts[0].to_i, parts[1].downcase, parts[2]

Check warning on line 501 in lib/fog/libvirt/models/compute/server.rb

View workflow job for this annotation

GitHub Actions / runner / rubocop

[rubocop] reported by reviewdog 🐶 Do not use parallel assignment. Raw Output: lib/fog/libvirt/models/compute/server.rb:501:15: C: Style/ParallelAssignment: Do not use parallel assignment.
if lease_mac == target_mac && expiry > best_expiry
best_expiry = expiry
best_ip = ip
end
end
rescue Errno::EACCES, Errno::ENOENT
return nil
end

best_ip
end

# This retrieves the ip address of the mac address using dhcp_leases
# It returns an array of public and private ip addresses
# Currently only one ip address is returned, but in the future this could be multiple
Expand All @@ -480,14 +521,23 @@
net = service.networks.all(:name => nic.network).first
# Assume the lease expiring last is the current IP address
ip_address = net&.dhcp_leases(nic.mac)&.max_by { |lse| lse["expirytime"] }&.dig("ipaddr")

# Fallback: when the network has no libvirt-managed DHCP (e.g. an
# external dnsmasq running in a network namespace on WSL2), the
# DHCPLeases API returns empty. Read the dnsmasq lease file directly.
if ip_address.nil? && net
@@leasefile_fallback_warned ||= {}

Check warning on line 529 in lib/fog/libvirt/models/compute/server.rb

View workflow job for this annotation

GitHub Actions / runner / rubocop

[rubocop] reported by reviewdog 🐶 Replace class var @@leasefile_fallback_warned with a class instance var. Raw Output: lib/fog/libvirt/models/compute/server.rb:529:15: C: Style/ClassVars: Replace class var @@leasefile_fallback_warned with a class instance var.
unless @@leasefile_fallback_warned[nic.mac]
Fog::Logger.warning("DHCPLeases API returned no address for #{nic.mac}; falling back to dnsmasq lease file.")
@@leasefile_fallback_warned[nic.mac] = true
end
ip_address = ip_address_from_leasefile(net, nic.mac)
end
end

return { :public => [ip_address], :private => [ip_address] }
end

# Locale-friendly removal of non-alpha nums
DOMAIN_CLEANUP_REGEXP = Regexp.compile('[\W_-]')

def ip_address(key)
addresses[key]&.first
end
Expand Down
86 changes: 86 additions & 0 deletions minitests/server/leasefile_fallback_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
require 'test_helper'
require 'tmpdir'

class LeasefileFallbackTest < Minitest::Test
def setup
@compute = Fog::Compute[:libvirt]
@server = @compute.servers.new(:name => "test")
@mac = "52:54:00:01:02:03"
@net = stub(:name => "default")
@tmpdir = Dir.mktmpdir("fog-leasefile-test")
@original_dir = Fog::Libvirt::Compute::Server::DNSMASQ_LEASE_DIR
Fog::Libvirt::Compute::Server.send(:remove_const, :DNSMASQ_LEASE_DIR)
Fog::Libvirt::Compute::Server.const_set(:DNSMASQ_LEASE_DIR, @tmpdir)
end

def teardown
FileUtils.remove_entry(@tmpdir)
Fog::Libvirt::Compute::Server.send(:remove_const, :DNSMASQ_LEASE_DIR)
Fog::Libvirt::Compute::Server.const_set(:DNSMASQ_LEASE_DIR, @original_dir)
end

def test_returns_ip_for_matching_mac
write_lease_file("default", "1000 52:54:00:01:02:03 192.168.122.10 host1 *\n")
result = @server.send(:ip_address_from_leasefile, @net, @mac)
assert_equal "192.168.122.10", result
end

def test_returns_ip_with_highest_expiry
content = <<~LEASES
1000 52:54:00:01:02:03 192.168.122.10 host1 *
2000 52:54:00:01:02:03 192.168.122.20 host1 *
1500 52:54:00:01:02:03 192.168.122.15 host1 *
LEASES
write_lease_file("default", content)
result = @server.send(:ip_address_from_leasefile, @net, @mac)
assert_equal "192.168.122.20", result
end

def test_mac_matching_is_case_insensitive
write_lease_file("default", "1000 52:54:00:01:02:03 192.168.122.10 host1 *\n")
result = @server.send(:ip_address_from_leasefile, @net, "52:54:00:01:02:03".upcase)
assert_equal "192.168.122.10", result
end

def test_returns_nil_when_lease_file_missing
result = @server.send(:ip_address_from_leasefile, @net, @mac)
assert_nil result
end

def test_skips_malformed_lines
content = <<~LEASES
short line
1000 52:54:00:01:02:03 192.168.122.10 host1 *
incomplete
LEASES
write_lease_file("default", content)
result = @server.send(:ip_address_from_leasefile, @net, @mac)
assert_equal "192.168.122.10", result
end

def test_returns_nil_when_no_mac_matches
write_lease_file("default", "1000 52:54:00:ff:ff:ff 192.168.122.99 other *\n")
result = @server.send(:ip_address_from_leasefile, @net, @mac)
assert_nil result
end

def test_returns_nil_on_permission_error
skip("Cannot test permission denial as root") if Process.uid == 0

Check warning on line 68 in minitests/server/leasefile_fallback_test.rb

View workflow job for this annotation

GitHub Actions / runner / rubocop

[rubocop] reported by reviewdog 🐶 Use `Process.uid.zero?` instead of `Process.uid == 0`. Raw Output: minitests/server/leasefile_fallback_test.rb:68:54: C: Style/NumericPredicate: Use `Process.uid.zero?` instead of `Process.uid == 0`.
write_lease_file("default", "1000 52:54:00:01:02:03 192.168.122.10 host1 *\n")
File.chmod(0o000, File.join(@tmpdir, "default.leases"))
result = @server.send(:ip_address_from_leasefile, @net, @mac)
assert_nil result
end

def test_returns_nil_when_net_name_is_nil
net_nil_name = stub(:name => nil)
result = @server.send(:ip_address_from_leasefile, net_nil_name, @mac)
assert_nil result
end

private

def write_lease_file(net_name, content)
File.write(File.join(@tmpdir, "#{net_name}.leases"), content)
end
end