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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice addition!

Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
# Verify TNF configuration with MAC-keyed fencing credentials placed in per-host directories

exec openshift-install agent create image --dir $WORK

stderr 'The rendezvous host IP \(node0 IP\) is 192.168.111.20'

exists $WORK/agent.x86_64.iso
exists $WORK/auth/kubeconfig
exists $WORK/auth/kubeadmin-password

# Verify per-host fencing credentials are placed in each host's directory
isoIgnitionContains agent.x86_64.iso /etc/assisted/hostconfig/master-0/fencing-credentials.yaml
isocmp agent.x86_64.iso /etc/assisted/hostconfig/master-0/fencing-credentials.yaml expected/master-0-fencing-credentials.yaml

isoIgnitionContains agent.x86_64.iso /etc/assisted/hostconfig/master-1/fencing-credentials.yaml
isocmp agent.x86_64.iso /etc/assisted/hostconfig/master-1/fencing-credentials.yaml expected/master-1-fencing-credentials.yaml

-- install-config.yaml --
apiVersion: v1
baseDomain: test.metalkube.org
controlPlane:
name: master
replicas: 2
fencing:
credentials:
- macAddress: "00:aa:bb:cc:dd:01"
address: redfish+https://192.168.111.1:8000/redfish/v1/Systems/abc123
username: admin
password: password123
certificateVerification: Disabled
- macAddress: "00:aa:bb:cc:dd:02"
address: redfish+https://192.168.111.2:8000/redfish/v1/Systems/def456
username: admin
password: password456
certificateVerification: Enabled
compute:
- name: worker
replicas: 0
metadata:
namespace: cluster0
name: ostest
networking:
clusterNetwork:
- cidr: 10.128.0.0/14
hostPrefix: 23
networkType: OVNKubernetes
machineNetwork:
- cidr: 192.168.111.0/24
serviceNetwork:
- 172.30.0.0/16
platform:
baremetal:
apiVips:
- 192.168.111.5
ingressVips:
- 192.168.111.4
sshKey: ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDK6UTEydcEKzuNdPaofn8Z2DwgHqdcionLZBiPf/zIRNco++etLsat7Avv7yt04DINQd5zjxIFgG8jblaUB5E5C9ClUcMwb52GO0ay2Y9v1uBv1a4WhI3peKktAzYNk0EBMQlJtXPjRMrC9ylBPh+DsBHMu+KmDnfk7PIwyN4efC8k5kSRuPWoNdme1rz2+umU8FSmaWTHIajrbspf4GQbsntA5kuKEtDbfoNCU97o2KrRnUbeg3a8hwSjfh3u6MhlnGcg5K2Ij+zivEsWGCLKYUtE1ErqwfIzwWmJ6jnV66XCQGHf4Q1iIxqF7s2a1q24cgG2Z/iDXfqXrCIfy4P7b/Ztak3bdT9jfAdVZtdO5/r7I+O5hYhF86ayFlDWzZWP/ByiSb+q4CQbfVgK3BMmiAv2MqLHdhesmD/SmIcoOWUF6rFmRKZVFFpKpt5ATNTgUJ3JRowoXrrDruVXClUGRiCS6Zabd1rZ3VmTchaPJwtzQMdfIWISXj+Ig+C4UK0=
pullSecret: '{"auths": {"quay.io": {"auth": "c3VwZXItc2VjcmV0Cg=="}}}'
featureSet: TechPreviewNoUpgrade

-- agent-config.yaml --
apiVersion: v1alpha1
metadata:
name: ostest
namespace: cluster0
rendezvousIP: 192.168.111.20
hosts:
- hostname: master-0
role: master
interfaces:
- name: eth0
macAddress: "00:aa:bb:cc:dd:01"
- hostname: master-1
role: master
interfaces:
- name: eth0
macAddress: "00:aa:bb:cc:dd:02"

-- expected/master-0-fencing-credentials.yaml --
credentials:
- macaddress: 00:aa:bb:cc:dd:01
username: admin
password: password123
address: redfish+https://192.168.111.1:8000/redfish/v1/Systems/abc123
certificateVerification: Disabled
-- expected/master-1-fencing-credentials.yaml --
credentials:
- macaddress: 00:aa:bb:cc:dd:02
username: admin
password: password456
address: redfish+https://192.168.111.2:8000/redfish/v1/Systems/def456
certificateVerification: Enabled
57 changes: 55 additions & 2 deletions pkg/asset/agent/agentconfig/agenthosts.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (

"github.com/pkg/errors"
"github.com/sirupsen/logrus"
goyaml "gopkg.in/yaml.v2"
"k8s.io/apimachinery/pkg/util/validation/field"
"sigs.k8s.io/yaml"

Expand All @@ -16,6 +17,7 @@ import (
agentAsset "github.com/openshift/installer/pkg/asset/agent"
"github.com/openshift/installer/pkg/asset/agent/joiner"
"github.com/openshift/installer/pkg/asset/agent/workflow"
"github.com/openshift/installer/pkg/types"
"github.com/openshift/installer/pkg/types/agent"
"github.com/openshift/installer/pkg/types/baremetal/validation"
"github.com/openshift/installer/pkg/validate"
Expand All @@ -41,8 +43,9 @@ type nmStateInterface struct {
// AgentHosts generates the hosts information from the AgentConfig and
// OptionalInstallConfig assets.
type AgentHosts struct {
Hosts []agent.Host
rendezvousIP string
Hosts []agent.Host
FencingCredentialsHost []types.Credential
rendezvousIP string
}

// Name returns a human friendly name.
Expand Down Expand Up @@ -93,6 +96,9 @@ func (a *AgentHosts) Generate(_ context.Context, dependencies asset.Parents) err
}
}

// store per host fencing-credentials only when MAC address is used
a.populateFencingCredentialHosts(installConfig)

case workflow.AgentWorkflowTypeAddNodes:
a.Hosts = append(a.Hosts, addNodesConfig.Config.Hosts...)
for i, host := range a.Hosts {
Expand Down Expand Up @@ -367,5 +373,52 @@ func (a *AgentHosts) HostConfigFiles() (HostConfigFileMap, error) {
files[filepath.Join(name, "role")] = []byte(host.Role)
}
}

maxNewDirs := len(a.Hosts)
for i := range a.FencingCredentialsHost {
cred := &a.FencingCredentialsHost[i]
dirName := findHostDirForMAC(files, cred.MACAddress)
if dirName == "" {
dirName = fmt.Sprintf("host-%d", maxNewDirs)
maxNewDirs++
files[filepath.Join(dirName, "mac_addresses")] = []byte(strings.ToLower(cred.MACAddress) + "\n")
}
cfg := &FencingCredentialsConfig{Credentials: []*types.Credential{cred}}
data, err := goyaml.Marshal(cfg)
if err != nil {
return nil, err
}
files[filepath.Join(dirName, "fencing-credentials.yaml")] = data
}

return files, nil
}

func (a *AgentHosts) populateFencingCredentialHosts(installConfig *agentAsset.OptionalInstallConfig) {
if installConfig.Config == nil || installConfig.Config.ControlPlane == nil ||
installConfig.Config.ControlPlane.Fencing == nil {
return
}

for _, cred := range installConfig.Config.ControlPlane.Fencing.Credentials {
if cred.HostName == "" && cred.MACAddress != "" {
a.FencingCredentialsHost = append(a.FencingCredentialsHost, *cred)
}
}
}

func findHostDirForMAC(files HostConfigFileMap, macAddress string) string {
normalizedMAC := strings.ToLower(macAddress)
for key, content := range files {
if !strings.HasSuffix(key, "/mac_addresses") {
continue
}
dirName := strings.TrimSuffix(key, "/mac_addresses")
for _, mac := range strings.Split(strings.TrimSpace(string(content)), "\n") {
if strings.TrimSpace(mac) == normalizedMAC {
return dirName
}
}
}
return ""
}
194 changes: 194 additions & 0 deletions pkg/asset/agent/agentconfig/agenthosts_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -955,6 +955,200 @@ func (ib *InterfacetBuilder) build() *aiv1beta1.Interface {
return &ib.Interface
}

func TestAgentHosts_FencingCredentialsHost(t *testing.T) {
cases := []struct {
name string
dependencies []asset.Asset
expectedCredentials []types.Credential
}{
{
name: "mac-keyed credentials are collected",
dependencies: []asset.Asset{
&workflow.AgentWorkflow{Workflow: workflow.AgentWorkflowTypeInstall},
&joiner.AddNodesConfig{},
getInstallConfigWithFencing([]*types.Credential{
{MACAddress: "28:d2:44:d2:b2:1a", Username: "admin", Password: "pass", Address: "redfish+https://10.0.0.1/redfish/v1/Systems/1"},
}),
getAgentConfigMultiHost("worker"),
},
expectedCredentials: []types.Credential{
{MACAddress: "28:d2:44:d2:b2:1a", Username: "admin", Password: "pass", Address: "redfish+https://10.0.0.1/redfish/v1/Systems/1"},
},
},
{
name: "hostname-keyed credentials are excluded",
dependencies: []asset.Asset{
&workflow.AgentWorkflow{Workflow: workflow.AgentWorkflowTypeInstall},
&joiner.AddNodesConfig{},
getInstallConfigWithFencing([]*types.Credential{
{HostName: "master-0", Username: "admin", Password: "pass", Address: "redfish+https://10.0.0.1/redfish/v1/Systems/1"},
}),
getAgentConfigMultiHost("worker"),
},
expectedCredentials: nil,
},
{
name: "mixed credentials only collect mac-keyed",
dependencies: []asset.Asset{
&workflow.AgentWorkflow{Workflow: workflow.AgentWorkflowTypeInstall},
&joiner.AddNodesConfig{},
getInstallConfigWithFencing([]*types.Credential{
{HostName: "master-0", Username: "admin", Password: "pass", Address: "redfish+https://10.0.0.1/redfish/v1/Systems/1"},
{MACAddress: "28:d2:44:d2:b2:1b", Username: "admin2", Password: "pass2", Address: "redfish+https://10.0.0.2/redfish/v1/Systems/1"},
}),
getAgentConfigMultiHost("worker"),
},
expectedCredentials: []types.Credential{
{MACAddress: "28:d2:44:d2:b2:1b", Username: "admin2", Password: "pass2", Address: "redfish+https://10.0.0.2/redfish/v1/Systems/1"},
},
},
{
name: "credential with both hostname and mac is treated as hostname-keyed",
dependencies: []asset.Asset{
&workflow.AgentWorkflow{Workflow: workflow.AgentWorkflowTypeInstall},
&joiner.AddNodesConfig{},
getInstallConfigWithFencing([]*types.Credential{
{HostName: "master-0", MACAddress: "28:d2:44:d2:b2:1a", Username: "admin", Password: "pass", Address: "redfish+https://10.0.0.1/redfish/v1/Systems/1"},
}),
getAgentConfigMultiHost("worker"),
},
expectedCredentials: nil,
},
{
name: "no fencing config produces nil",
dependencies: []asset.Asset{
&workflow.AgentWorkflow{Workflow: workflow.AgentWorkflowTypeInstall},
&joiner.AddNodesConfig{},
getInstallConfigSingleHost(),
getAgentConfigSingleHost(),
},
expectedCredentials: nil,
},
}

for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
parents := asset.Parents{}
parents.Add(tc.dependencies...)

ah := &AgentHosts{}
err := ah.Generate(context.Background(), parents)
assert.NoError(t, err)
assert.Equal(t, tc.expectedCredentials, ah.FencingCredentialsHost)
})
}
}

func TestHostConfigFiles_FencingCredentials(t *testing.T) {
cases := []struct {
name string
hosts []agent.Host
credentials []types.Credential
expectedFiles map[string]string
}{
{
name: "mac matches existing host directory",
hosts: []agent.Host{
{
Hostname: "master-0",
Interfaces: []*aiv1beta1.Interface{{MacAddress: "aa:bb:cc:dd:ee:01"}},
},
},
credentials: []types.Credential{
{MACAddress: "AA:BB:CC:DD:EE:01", Username: "admin", Password: "pass", Address: "redfish+https://10.0.0.1/redfish/v1/Systems/1"},
},
expectedFiles: map[string]string{
"master-0/fencing-credentials.yaml": "username: admin",
},
},
{
name: "mac with no matching host creates new directory",
hosts: []agent.Host{
{
Hostname: "master-0",
Interfaces: []*aiv1beta1.Interface{{MacAddress: "aa:bb:cc:dd:ee:01"}},
},
},
credentials: []types.Credential{
{MACAddress: "FF:FF:FF:FF:FF:FF", Username: "admin", Password: "pass", Address: "redfish+https://10.0.0.1/redfish/v1/Systems/1"},
},
expectedFiles: map[string]string{
"host-1/fencing-credentials.yaml": "username: admin",
"host-1/mac_addresses": "ff:ff:ff:ff:ff:ff",
},
},
{
name: "case-insensitive mac matching",
hosts: []agent.Host{
{
Hostname: "master-0",
Interfaces: []*aiv1beta1.Interface{{MacAddress: "aa:bb:cc:dd:ee:01"}},
},
},
credentials: []types.Credential{
{MACAddress: "AA:BB:CC:DD:EE:01", Username: "admin", Password: "pass", Address: "redfish+https://10.0.0.1/redfish/v1/Systems/1"},
},
expectedFiles: map[string]string{
"master-0/fencing-credentials.yaml": "username: admin",
},
},
{
name: "new dir avoids collision with existing host-0",
hosts: []agent.Host{
{
Interfaces: []*aiv1beta1.Interface{{MacAddress: "aa:bb:cc:dd:ee:01"}},
},
},
credentials: []types.Credential{
{MACAddress: "FF:FF:FF:FF:FF:FF", Username: "admin", Password: "pass", Address: "redfish+https://10.0.0.1/redfish/v1/Systems/1"},
},
expectedFiles: map[string]string{
"host-1/fencing-credentials.yaml": "username: admin",
"host-1/mac_addresses": "ff:ff:ff:ff:ff:ff",
},
},
{
name: "new dir avoids collision with empty host that has no files",
hosts: []agent.Host{
{},
},
credentials: []types.Credential{
{MACAddress: "FF:FF:FF:FF:FF:FF", Username: "admin", Password: "pass", Address: "redfish+https://10.0.0.1/redfish/v1/Systems/1"},
},
expectedFiles: map[string]string{
"host-1/fencing-credentials.yaml": "username: admin",
"host-1/mac_addresses": "ff:ff:ff:ff:ff:ff",
},
},
}

for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
ah := &AgentHosts{
Hosts: tc.hosts,
FencingCredentialsHost: tc.credentials,
}

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

for path, expectedContent := range tc.expectedFiles {
data, ok := files[path]
assert.True(t, ok, "expected file at %s", path)
assert.Contains(t, string(data), expectedContent)
}
})
}
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

func getInstallConfigWithFencing(credentials []*types.Credential) *agentAsset.OptionalInstallConfig {
ic := getNoHostsInstallConfig()
ic.Config.ControlPlane.Fencing = &types.Fencing{
Credentials: credentials,
}
return ic
}

func getInstallConfigWithMismatchedNetworkConfig() *agentAsset.OptionalInstallConfig {
a := getInstallConfigSingleHost()
a.Config.Platform.BareMetal.Hosts[0].NetworkConfig = &apiextv1.JSON{
Expand Down
2 changes: 2 additions & 0 deletions pkg/asset/agent/agentconfig/fencingcredentials_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -442,6 +442,8 @@ func TestFencingCredentials_YAMLFormat(t *testing.T) {
assert.NotContains(t, yamlContent, "macaddress:")
})

// This subtest verifies YAML format only. Placement into per-host directories
// for MAC-keyed credentials is handled by addFencingCredentials() in ignition.go.
t.Run("with macaddress", func(t *testing.T) {
parents := asset.Parents{}
parents.Add(
Expand Down
Loading