Skip to content
Merged
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
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,13 @@
### 3.1.0 / 2026-07-02
* Added:
* `ensure_beaker_ip_on`: detect SUTs whose recorded `host[:ip]` was never
actually applied (e.g. EL10 under Vagrant, where the legacy ifcfg files
Vagrant writes for static private-network IPs are no longer supported and
the interface silently falls back to DHCP) and (re-)apply the expected
address via NetworkManager (reboot-safe), falling back to `ip addr add`.
Runs automatically alongside `activate_interfaces` in the global
`before(:all)` hook; disable with `BEAKER_no_fix_interfaces`.

### 3.0.0 / 2026-06-24
* Changed (**Breaking**):
* Dropped support for Puppet 7 / Ruby 2.7. CI now tests Puppet/OpenVox 8 only,
Expand Down
62 changes: 62 additions & 0 deletions lib/simp/beaker_helpers.rb
Original file line number Diff line number Diff line change
Expand Up @@ -1138,6 +1138,64 @@ def activate_interfaces(hosts)
end
end

# Ensure that the IP address that Beaker has recorded for each SUT is
# actually present on the system and (re-)apply it if it is not.
#
# Some OS/hypervisor combinations silently fail to configure the static IP
# for secondary (private) networks. In particular, Vagrant configures
# static IPs on RedHat-family guests by writing legacy ifcfg files, which
# EL10 no longer supports at all. The interface falls back to NetworkManager
# DHCP and receives an arbitrary address, so everything that targets the
# recorded `host[:ip]` (cross-SUT traffic, direct Net::SSH logins, etc.)
# fails with 'No route to host'.
#
# This detects the mismatch and applies the expected address to the
# interface that is attached to the same network, using NetworkManager so
# that the address survives reboots, and falling back to a runtime-only
# `ip addr add` when NetworkManager is not in use.
def ensure_beaker_ip_on(hosts)
return if ENV['BEAKER_no_fix_interfaces']

block_on(hosts, run_in_parallel: @run_in_parallel) do |host|
expected_ip = host[:ip]
next unless expected_ip
next if host[:platform].include?('windows')

# All currently assigned IPv4 addresses ('<iface> <address>/<prefix>')
addrs = on(host, %(ip -o -4 addr show), accept_all_exit_codes: true).stdout.lines.map { |line|
next unless line =~ %r{^\d+:\s+(\S+)\s+inet\s+(\S+)/(\d+)}
{ iface: Regexp.last_match(1), ip: Regexp.last_match(2), prefix: Regexp.last_match(3) }
}.compact

next if addrs.any? { |a| a[:ip] == expected_ip }

require 'ipaddr'
target = addrs.find do |a|
IPAddr.new("#{a[:ip]}/#{a[:prefix]}").include?(expected_ip)
rescue IPAddr::Error
false
end

unless target
puts " -- WARNING: #{host} does not have expected IP #{expected_ip} and no interface on that network could be found"
next
end

puts " -- FIXING: #{host} expected IP #{expected_ip} but '#{target[:iface]}' has #{target[:ip]}"

nm_connection = on(host, %(nmcli -g GENERAL.CONNECTION device show #{target[:iface]}), accept_all_exit_codes: true).stdout.strip
if nm_connection.empty?
# Not NetworkManager-managed => runtime-only fallback
on(host, %(ip addr add #{expected_ip}/#{target[:prefix]} dev #{target[:iface]}), accept_all_exit_codes: true)
else
# Match Vagrant's static network behavior: fixed address, no gateway,
# and never a default route on the private interface
on(host, %(nmcli connection modify "#{nm_connection}" ipv4.method manual ipv4.addresses "#{expected_ip}/#{target[:prefix]}" ipv4.gateway "" ipv4.never-default yes))
on(host, %(nmcli connection up "#{nm_connection}"))
end
end
end

## Inline Hiera Helpers ##
## These will be integrated into core Beaker at some point ##

Expand All @@ -1152,6 +1210,10 @@ def activate_interfaces(hosts)

# We can't guarantee that the upstream vendor isn't disabling interfaces
activate_interfaces(hosts)

# Some OS/hypervisor combinations (e.g. EL10 under Vagrant) fail to
# apply the static IP that Beaker expects for private networks
ensure_beaker_ip_on(hosts)
end

c.after(:all) do
Expand Down
2 changes: 1 addition & 1 deletion lib/simp/beaker_helpers/version.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,5 @@
module Simp; end

module Simp::BeakerHelpers # rubocop:disable Style/OneClassPerFile
VERSION = '3.0.0'
VERSION = '3.1.0'
end
1 change: 1 addition & 0 deletions spec/lib/simp/beaker_helpers_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
# redefine methods used in RSpec.configure withing Simp::BeakerHelpers
def hosts; end
def activate_interfaces(hosts); end
def ensure_beaker_ip_on(hosts); end
def clear_temp_hieradata; end

class MyTestClass
Expand Down