Skip to content
Closed
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
54 changes: 54 additions & 0 deletions pkg/asset/agent/agentconfig/agenthosts.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import (
"github.com/openshift/installer/pkg/asset/agent/joiner"
"github.com/openshift/installer/pkg/asset/agent/workflow"
"github.com/openshift/installer/pkg/types/agent"
"github.com/openshift/installer/pkg/types/baremetal"
"github.com/openshift/installer/pkg/types/baremetal/validation"
"github.com/openshift/installer/pkg/validate"
)
Expand Down Expand Up @@ -296,6 +297,48 @@ type HostConfigFileMap map[string][]byte

// HostConfigFiles returns a map from filename to contents of the files used for
// host-specific configuration by the agent installer client.
// generateFencingCredentialsYAML creates the YAML content for fencing-credentials.yaml
// from a baremetal.BMC struct. It converts the BMC structure to the format expected
// by the assisted-service agent-based installer client.
func generateFencingCredentialsYAML(bmc *baremetal.BMC) ([]byte, error) {
if bmc == nil {
return nil, errors.New("BMC cannot be nil")
}

// Skip generation if BMC address is not set
if bmc.Address == "" {
return nil, nil
}

// Convert bool to string enum for certificateVerification
// BMC uses DisableCertificateVerification (bool), but the fencing-credentials.yaml
// uses certificateVerification (string: "Enabled"|"Disabled")
certVerification := "Enabled" // default is enabled (DisableCertificateVerification = false)
if bmc.DisableCertificateVerification {
certVerification = "Disabled"
}

// Create intermediate structure for YAML marshaling
fcYAML := struct {
Address string `yaml:"address"`
Username string `yaml:"username"`
Password string `yaml:"password"`
CertificateVerification *string `yaml:"certificateVerification,omitempty"`
}{
Address: bmc.Address,
Username: bmc.Username,
Password: bmc.Password,
CertificateVerification: &certVerification,
}

data, err := yaml.Marshal(fcYAML)
if err != nil {
return nil, fmt.Errorf("failed to marshal fencing credentials: %w", err)
}

return data, nil
}

func (a *AgentHosts) HostConfigFiles() (HostConfigFileMap, error) {
if a == nil {
return nil, nil
Expand Down Expand Up @@ -328,6 +371,17 @@ func (a *AgentHosts) HostConfigFiles() (HostConfigFileMap, error) {
if len(host.Role) > 0 {
files[filepath.Join(name, "role")] = []byte(host.Role)
}

// Generate fencing-credentials.yaml if BMC is configured
if host.BMC.Address != "" {
fcData, err := generateFencingCredentialsYAML(&host.BMC)
if err != nil {
return nil, fmt.Errorf("failed to generate fencing credentials for host %s: %w", name, err)
}
if fcData != nil {
files[filepath.Join(name, "fencing-credentials.yaml")] = fcData
}
}
}
return files, nil
}
244 changes: 244 additions & 0 deletions pkg/asset/agent/agentconfig/agenthosts_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
apiextv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/utils/pointer"
"sigs.k8s.io/yaml"

aiv1beta1 "github.com/openshift/assisted-service/api/v1beta1"
"github.com/openshift/installer/pkg/asset"
Expand Down Expand Up @@ -753,3 +754,246 @@ func iface(name string, mac string) *InterfacetBuilder {
func (ib *InterfacetBuilder) build() *aiv1beta1.Interface {
return &ib.Interface
}

func TestGenerateFencingCredentialsYAML(t *testing.T) {
tests := []struct {
name string
bmc *baremetal.BMC
expectedCertVerify string
expectError bool
expectNil bool
}{
{
name: "BMC with certificate verification disabled",
bmc: &baremetal.BMC{
Address: "redfish+https://192.168.1.1:8000/redfish/v1/Systems/1",
Username: "admin",
Password: "password",
DisableCertificateVerification: true,
},
expectedCertVerify: "Disabled",
expectError: false,
expectNil: false,
},
{
name: "BMC with certificate verification enabled",
bmc: &baremetal.BMC{
Address: "redfish+https://192.168.1.1:8000/redfish/v1/Systems/1",
Username: "admin",
Password: "password",
DisableCertificateVerification: false,
},
expectedCertVerify: "Enabled",
expectError: false,
expectNil: false,
},
{
name: "Nil BMC",
bmc: nil,
expectError: true,
expectNil: false,
},
{
name: "BMC with empty address",
bmc: &baremetal.BMC{
Address: "",
Username: "admin",
Password: "password",
},
expectError: false,
expectNil: true,
},
{
name: "BMC with various redfish schemes",
bmc: &baremetal.BMC{
Address: "idrac-redfish+https://192.168.1.1:8000/redfish/v1/Systems/1",
Username: "admin",
Password: "password",
DisableCertificateVerification: false,
},
expectedCertVerify: "Enabled",
expectError: false,
expectNil: false,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
data, err := generateFencingCredentialsYAML(tt.bmc)

if tt.expectError {
assert.Error(t, err)
return
}

assert.NoError(t, err)

if tt.expectNil {
assert.Nil(t, data)
return
}

// Unmarshal and verify structure
var fc struct {
Address string `yaml:"address"`
Username string `yaml:"username"`
Password string `yaml:"password"`
CertificateVerification *string `yaml:"certificateVerification"`
}

err = yaml.Unmarshal(data, &fc)
assert.NoError(t, err)

assert.Equal(t, tt.bmc.Address, fc.Address)
assert.Equal(t, tt.bmc.Username, fc.Username)
assert.Equal(t, tt.bmc.Password, fc.Password)
assert.NotNil(t, fc.CertificateVerification)
assert.Equal(t, tt.expectedCertVerify, *fc.CertificateVerification)
})
}
}

func TestHostConfigFiles_WithFencingCredentials(t *testing.T) {
agentHosts := &AgentHosts{
Hosts: []agent.Host{
{
Hostname: "master-0",
Role: "master",
BMC: baremetal.BMC{
Address: "redfish+https://192.168.1.1:8000/redfish/v1/Systems/1",
Username: "admin",
Password: "password",
DisableCertificateVerification: true,
},
RootDeviceHints: baremetal.RootDeviceHints{
DeviceName: "/dev/sda",
},
Interfaces: []*aiv1beta1.Interface{
{
Name: "eth0",
MacAddress: "00:11:22:33:44:55",
},
},
},
},
}

files, err := agentHosts.HostConfigFiles()
assert.NoError(t, err)

// Verify fencing-credentials.yaml was generated
fcPath := "master-0/fencing-credentials.yaml"
assert.Contains(t, files, fcPath)

// Verify content
var fc struct {
Address string `yaml:"address"`
Username string `yaml:"username"`
Password string `yaml:"password"`
CertificateVerification *string `yaml:"certificateVerification"`
}

err = yaml.Unmarshal(files[fcPath], &fc)
assert.NoError(t, err)
assert.Equal(t, "redfish+https://192.168.1.1:8000/redfish/v1/Systems/1", fc.Address)
assert.Equal(t, "admin", fc.Username)
assert.Equal(t, "password", fc.Password)
assert.NotNil(t, fc.CertificateVerification)
assert.Equal(t, "Disabled", *fc.CertificateVerification)

// Verify other hostconfig files were also generated
assert.Contains(t, files, "master-0/mac_addresses")
assert.Contains(t, files, "master-0/root-device-hints.yaml")
assert.Contains(t, files, "master-0/role")
}

func TestHostConfigFiles_WithoutBMC(t *testing.T) {
agentHosts := &AgentHosts{
Hosts: []agent.Host{
{
Hostname: "worker-0",
Role: "worker",
// No BMC configured
Interfaces: []*aiv1beta1.Interface{
{
Name: "eth0",
MacAddress: "00:11:22:33:44:66",
},
},
},
},
}

files, err := agentHosts.HostConfigFiles()
assert.NoError(t, err)

// Verify fencing-credentials.yaml was NOT generated for hosts without BMC
fcPath := "worker-0/fencing-credentials.yaml"
assert.NotContains(t, files, fcPath)

// But other files should still be generated
assert.Contains(t, files, "worker-0/mac_addresses")
assert.Contains(t, files, "worker-0/role")
}

func TestHostConfigFiles_MultiplHostsWithMixedBMC(t *testing.T) {
agentHosts := &AgentHosts{
Hosts: []agent.Host{
{
Hostname: "master-0",
Role: "master",
BMC: baremetal.BMC{
Address: "redfish+https://192.168.1.1:8000/redfish/v1/Systems/1",
Username: "admin",
Password: "password",
DisableCertificateVerification: false,
},
Interfaces: []*aiv1beta1.Interface{
{MacAddress: "00:11:22:33:44:55"},
},
},
{
Hostname: "master-1",
Role: "master",
BMC: baremetal.BMC{
Address: "redfish+https://192.168.1.2:8000/redfish/v1/Systems/2",
Username: "admin",
Password: "password",
DisableCertificateVerification: true,
},
Interfaces: []*aiv1beta1.Interface{
{MacAddress: "00:11:22:33:44:56"},
},
},
{
Hostname: "worker-0",
Role: "worker",
// No BMC
Interfaces: []*aiv1beta1.Interface{
{MacAddress: "00:11:22:33:44:57"},
},
},
},
}

files, err := agentHosts.HostConfigFiles()
assert.NoError(t, err)

// Verify both masters have fencing-credentials.yaml
assert.Contains(t, files, "master-0/fencing-credentials.yaml")
assert.Contains(t, files, "master-1/fencing-credentials.yaml")

// Verify worker does NOT have fencing-credentials.yaml
assert.NotContains(t, files, "worker-0/fencing-credentials.yaml")

// Verify different certificate verification settings
var fc0, fc1 struct {
CertificateVerification *string `yaml:"certificateVerification"`
}

yaml.Unmarshal(files["master-0/fencing-credentials.yaml"], &fc0)
yaml.Unmarshal(files["master-1/fencing-credentials.yaml"], &fc1)

assert.Equal(t, "Enabled", *fc0.CertificateVerification)
assert.Equal(t, "Disabled", *fc1.CertificateVerification)
}
39 changes: 39 additions & 0 deletions pkg/types/baremetal/validation/platform.go
Original file line number Diff line number Diff line change
Expand Up @@ -437,6 +437,41 @@ func validateProvisioningNetworkDisabledSupported(hosts []*baremetal.Host, fldPa
return
}

// validateHostsBMCForFencing validates BMC addresses are RedFish-compatible for fencing (TNF).
// This is required for Two-Node Fencing configurations where Pacemaker needs to fence nodes
// via RedFish-compatible BMCs.
func validateHostsBMCForFencing(hosts []*baremetal.Host, installConfig *types.InstallConfig, fldPath *field.Path) field.ErrorList {
errors := field.ErrorList{}

// Only validate if this is a TNF cluster (2 control plane replicas with fencing enabled)
if installConfig.ControlPlane == nil || installConfig.ControlPlane.Replicas == nil {
return errors
}

// TNF requires exactly 2 control plane nodes and fencing credentials
isTNF := *installConfig.ControlPlane.Replicas == 2 &&
installConfig.ControlPlane.Fencing != nil &&
len(installConfig.ControlPlane.Fencing.Credentials) > 0

if !isTNF {
return errors
}

// For TNF clusters, validate that control plane BMC addresses are RedFish-compatible
for idx, host := range hosts {
if !host.IsMaster() {
continue // Only validate control plane hosts
}

// Use the shared RedFish BMC validation function from types/common package
if validationErrs := common.ValidateRedfishBMCAddress(host.BMC.Address, fldPath.Index(idx).Child("bmc").Child("address")); len(validationErrs) > 0 {
errors = append(errors, validationErrs...)
}
}

return errors
}

// ValidatePlatform checks that the specified platform is valid.
func ValidatePlatform(p *baremetal.Platform, agentBasedInstallation bool, n *types.Networking, fldPath *field.Path, c *types.InstallConfig) field.ErrorList {
allErrs := field.ErrorList{}
Expand Down Expand Up @@ -499,6 +534,10 @@ func ValidateHosts(p *baremetal.Platform, fldPath *field.Path, c *types.InstallC
allErrs = append(allErrs, validateNetworkConfig(p.Hosts, fldPath)...)

allErrs = append(allErrs, validateHostsName(p.Hosts, fldPath)...)

// Validate BMC addresses for TNF (Two-Node Fencing) clusters
allErrs = append(allErrs, validateHostsBMCForFencing(p.Hosts, c, fldPath)...)

return allErrs
}

Expand Down
Loading