From bbaac32175bd3012a16bf9b0b087c43e28efa77d Mon Sep 17 00:00:00 2001 From: Pat Riehecky Date: Fri, 27 Feb 2026 10:28:51 -0600 Subject: [PATCH] Added control for interface failover on IPMI --- REFERENCE.md | 12 +++ manifests/network.pp | 66 ++++++++++++++-- spec/defines/ipmi_network_spec.rb | 120 ++++++++++++++++++++++++++++-- 3 files changed, 185 insertions(+), 13 deletions(-) diff --git a/REFERENCE.md b/REFERENCE.md index e65d56f..b46ef97 100644 --- a/REFERENCE.md +++ b/REFERENCE.md @@ -128,6 +128,7 @@ The following parameters are available in the `ipmi::network` defined type: * [`gateway`](#-ipmi--network--gateway) * [`type`](#-ipmi--network--type) * [`lan_channel`](#-ipmi--network--lan_channel) +* [`interface_type`](#-ipmi--network--interface_type) ##### `ip` @@ -170,6 +171,17 @@ Defaults to the first detected lan channel, starting at 1 ending at 11 Default value: `undef` +##### `interface_type` + +Data type: `Optional[Enum['dedicated', 'shared', 'failover']]` + +Controls the NIC selection mode for the BMC. +Supported values: 'dedicated', 'shared', 'failover'. +Only applied when the manufacturer is known (Supermicro or Dell). +Leave undef to skip interface type configuration. + +Default value: `undef` + ### `ipmi::snmp` Manage SNMP community strings diff --git a/manifests/network.pp b/manifests/network.pp index 32338e9..0057389 100644 --- a/manifests/network.pp +++ b/manifests/network.pp @@ -11,18 +11,24 @@ # @param lan_channel # Controls the lan channel of the IPMI network to be configured. # Defaults to the first detected lan channel, starting at 1 ending at 11 +# @param interface_type +# Controls the NIC selection mode for the BMC. +# Supported values: 'dedicated', 'shared', 'failover'. +# Only applied when the manufacturer is known (Supermicro or Dell). +# Leave undef to skip interface type configuration. # define ipmi::network ( - Stdlib::IP::Address $ip = '0.0.0.0', - Stdlib::IP::Address $netmask = '255.255.255.0', - Stdlib::IP::Address $gateway = '0.0.0.0', - Enum['dhcp', 'static'] $type = 'dhcp', - Optional[Integer] $lan_channel = undef + Stdlib::IP::Address $ip = '0.0.0.0', + Stdlib::IP::Address $netmask = '255.255.255.0', + Stdlib::IP::Address $gateway = '0.0.0.0', + Enum['dhcp', 'static'] $type = 'dhcp', + Optional[Integer] $lan_channel = undef, + Optional[Enum['dedicated', 'shared', 'failover']] $interface_type = undef, ) { require ipmi::install $_real_lan_channel = $lan_channel ? { - undef => $ipmi::default_channel, + undef => $ipmi::default_channel, default => $lan_channel, } @@ -62,4 +68,52 @@ 'Subnet Mask' | sed -e 's/.* : //g')\" != \"${netmask}\"", } } + + if $interface_type != undef { + $_manufacturer = $facts.dig('ipmitool_mc_info', 'Manufacturer Name') + if $_manufacturer != undef { + case $_manufacturer { + 'Supermicro': { + $_raw_check = '0x30 0x70 0x0c' + case $interface_type { + 'dedicated': { $_iface_code = 0 } + 'shared': { $_iface_code = 1 } + 'failover': { $_iface_code = 2 } + default: { $_iface_code = undef } + } + if $_iface_code != undef { + # lint:ignore:140chars + exec { "ipmi_set_interface_type_${_real_lan_channel}": + command => "/usr/bin/ipmitool raw ${_raw_check} 1 ${_iface_code}", + onlyif => "/usr/bin/test $(/usr/bin/ipmitool raw ${_raw_check} 0) -ne ${_iface_code}", + } + # lint:endignore + } + } + 'Dell': { + $_raw_set = '0x30 0x24' + $_raw_check = '0x30 0x25' + case $interface_type { + 'dedicated': { $_iface_code = 2 } + 'shared': { $_iface_code = 0 } + 'failover': { $_iface_code = 1 } + default: { $_iface_code = undef } + } + if $_iface_code != undef { + # lint:ignore:140chars + exec { "ipmi_set_interface_type_${_real_lan_channel}": + command => "/usr/bin/ipmitool raw ${_raw_set} ${_iface_code}", + onlyif => "/usr/bin/test $(/usr/bin/ipmitool raw ${_raw_check}) -ne ${_iface_code}", + } + # lint:endignore + } + } + default: { + # lint:ignore:140chars + warning("${_manufacturer} does not have interface type behavior defined in this module. Can you submit it?") + # lint:endignore + } + } + } + } } diff --git a/spec/defines/ipmi_network_spec.rb b/spec/defines/ipmi_network_spec.rb index 09e6e6e..cda5001 100644 --- a/spec/defines/ipmi_network_spec.rb +++ b/spec/defines/ipmi_network_spec.rb @@ -5,24 +5,24 @@ describe 'ipmi::network', type: :define do on_supported_os.each do |os, facts| context "on #{os}" do + let(:title) { 'example' } + + # Base facts without manufacturer — used by most existing tests. + # Interface type tests override this via subject-level let(:facts). let(:facts) do facts.merge( { ipmitool_mc_info: { IPMI_Puppet_Service_Recommend: 'running' }, - ipmi: { default: { channel: 1 } } + ipmi: { default: { channel: 1 } }, } ) end - let(:title) { 'example' } describe 'when deploying as dhcp with minimal params' do - let(:params) do - { - type: 'dhcp', - } - end + let(:params) { { type: 'dhcp' } } it { is_expected.to contain_exec('ipmi_set_dhcp_1') } + it { is_expected.not_to contain_exec('ipmi_set_interface_type_1') } end describe 'when deploying as dhcp with all params' do @@ -37,6 +37,7 @@ end it { is_expected.to contain_exec('ipmi_set_dhcp_1') } + it { is_expected.not_to contain_exec('ipmi_set_interface_type_1') } end describe 'when deploying as static with minimal params' do @@ -52,6 +53,7 @@ it { is_expected.to contain_exec('ipmi_set_static_1').that_notifies('Exec[ipmi_set_ipaddr_1]') } it { is_expected.to contain_exec('ipmi_set_static_1').that_notifies('Exec[ipmi_set_defgw_1]') } it { is_expected.to contain_exec('ipmi_set_static_1').that_notifies('Exec[ipmi_set_netmask_1]') } + it { is_expected.not_to contain_exec('ipmi_set_interface_type_1') } end describe 'when deploying as static with all params' do @@ -68,6 +70,110 @@ it { is_expected.to contain_exec('ipmi_set_static_2').that_notifies('Exec[ipmi_set_ipaddr_2]') } it { is_expected.to contain_exec('ipmi_set_static_2').that_notifies('Exec[ipmi_set_defgw_2]') } it { is_expected.to contain_exec('ipmi_set_static_2').that_notifies('Exec[ipmi_set_netmask_2]') } + it { is_expected.not_to contain_exec('ipmi_set_interface_type_2') } + end + + describe 'interface_type' do + # Shared params for all interface_type tests — type itself is irrelevant + # to interface configuration, so use dhcp to keep tests focused. + let(:base_params) { { type: 'dhcp', lan_channel: 1 } } + + describe 'when manufacturer fact is absent' do + let(:params) { base_params.merge(interface_type: 'dedicated') } + + # ipmitool_mc_info present but no Manufacturer Name key + it { is_expected.not_to contain_exec('ipmi_set_interface_type_1') } + end + + describe 'when manufacturer is unknown' do + let(:facts) do + facts.merge( + { + ipmitool_mc_info: { + IPMI_Puppet_Service_Recommend: 'running', + 'Manufacturer Name' => 'HPE', + }, + ipmi: { default: { channel: 1 } }, + } + ) + end + let(:params) { base_params.merge(interface_type: 'dedicated') } + + it { is_expected.not_to contain_exec('ipmi_set_interface_type_1') } + it { is_expected.to compile.with_warnings } + end + + describe 'Supermicro' do + let(:facts) do + facts.merge( + { + ipmitool_mc_info: { + IPMI_Puppet_Service_Recommend: 'running', + 'Manufacturer Name' => 'Supermicro', + }, + ipmi: { default: { channel: 1 } }, + } + ) + end + + { 'dedicated' => 0, 'shared' => 1, 'failover' => 2 }.each do |iface_type, code| + describe "interface_type => #{iface_type}" do + let(:params) { base_params.merge(interface_type: iface_type) } + + it { is_expected.to contain_exec('ipmi_set_interface_type_1') + .with_command("/usr/bin/ipmitool raw 0x30 0x70 0x0c 1 #{code}") + .with_onlyif("/usr/bin/test $(/usr/bin/ipmitool raw 0x30 0x70 0x0c 0) -ne #{code}") } + end + end + end + + describe 'Dell' do + let(:facts) do + facts.merge( + { + ipmitool_mc_info: { + IPMI_Puppet_Service_Recommend: 'running', + 'Manufacturer Name' => 'Dell', + }, + ipmi: { default: { channel: 1 } }, + } + ) + end + + { 'dedicated' => 2, 'shared' => 0, 'failover' => 1 }.each do |iface_type, code| + describe "interface_type => #{iface_type}" do + let(:params) { base_params.merge(interface_type: iface_type) } + + it { is_expected.to contain_exec('ipmi_set_interface_type_1') + .with_command("/usr/bin/ipmitool raw 0x30 0x24 #{code}") + .with_onlyif("/usr/bin/test $(/usr/bin/ipmitool raw 0x30 0x25) -ne #{code}") } + end + end + end + + describe 'when interface_type is undef' do + let(:facts) do + facts.merge( + { + ipmitool_mc_info: { + IPMI_Puppet_Service_Recommend: 'running', + 'Manufacturer Name' => 'Supermicro', + }, + ipmi: { default: { channel: 1 } }, + } + ) + end + let(:params) { base_params } + + # Manufacturer present but interface_type not requested — no exec created + it { is_expected.not_to contain_exec('ipmi_set_interface_type_1') } + end + + describe 'invalid interface_type value' do + let(:params) { base_params.merge(interface_type: 'bonded') } + + it { is_expected.to compile.and_raise_error(%r{interface_type}) } + end end end end