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
156 changes: 156 additions & 0 deletions cmd/agentbasedinstaller/host_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,15 +31,97 @@ const (
AgentWorkflowTypeAddNodes AgentWorkflowType = "addnodes"
)

// getHostConfigDir returns the host configuration directory path.
// It reads from the HOST_CONFIG_DIR environment variable, falling back
// to the default path if not set.
func getHostConfigDir() string {
if dir := os.Getenv("HOST_CONFIG_DIR"); dir != "" {
return dir
}
return "/etc/assisted/hostconfig"
}

// loadFencingCredentials reads the single fencing-credentials.yaml file from the hostconfig
// directory and returns a map of hostname→credentials for easy lookup during host application.
// Returns nil map (not error) if file doesn't exist, since fencing is optional.
func loadFencingCredentials(hostConfigDir string) (map[string]*models.FencingCredentialsParams, error) {
fencingFilePath := filepath.Join(hostConfigDir, "fencing-credentials.yaml")

fileData, err := os.ReadFile(fencingFilePath)
if err != nil {
if os.IsNotExist(err) {
log.Info("No fencing credentials file found, skipping fencing configuration")
return nil, nil // Not an error - fencing is optional
}
return nil, fmt.Errorf("failed to read fencing credentials file: %w", err)
}

// Intermediate structure matching installer's YAML output
type fencingCredentialsFile struct {
Credentials []struct {
Hostname string `yaml:"hostname"`
Address *string `yaml:"address"`
Username *string `yaml:"username"`
Password *string `yaml:"password"`
CertificateVerification *string `yaml:"certificateVerification,omitempty"`
} `yaml:"credentials"`
}

fcFile := &fencingCredentialsFile{}
if err := yaml.UnmarshalStrict(fileData, fcFile); err != nil {
return nil, fmt.Errorf("failed to parse fencing credentials file: %w", err)
}

credentialsMap := make(map[string]*models.FencingCredentialsParams)

for i, cred := range fcFile.Credentials {
if cred.Hostname == "" {
return nil, fmt.Errorf("fencing credential at index %d has empty hostname", i)
}

if _, exists := credentialsMap[cred.Hostname]; exists {
return nil, fmt.Errorf("duplicate fencing credential for hostname: %s", cred.Hostname)
}

if cred.Address == nil {
return nil, fmt.Errorf("fencing credential for hostname %s is missing required field: address", cred.Hostname)
}
if cred.Username == nil {
return nil, fmt.Errorf("fencing credential for hostname %s is missing required field: username", cred.Hostname)
}
if cred.Password == nil {
return nil, fmt.Errorf("fencing credential for hostname %s is missing required field: password", cred.Hostname)
}

credentialsMap[cred.Hostname] = &models.FencingCredentialsParams{
Address: cred.Address,
Username: cred.Username,
Password: cred.Password,
CertificateVerification: cred.CertificateVerification,
}
log.Infof("Loaded fencing credential for hostname: %s", cred.Hostname)
}

log.Infof("Loaded %d fencing credentials from file", len(credentialsMap))
return credentialsMap, nil
}

func ApplyHostConfigs(ctx context.Context, log *log.Logger, bmInventory *client.AssistedInstall, hostConfigs HostConfigs, infraEnvID strfmt.UUID) ([]Failure, error) {
hostList, err := bmInventory.Installer.V2ListHosts(ctx, installer.NewV2ListHostsParams().WithInfraEnvID(infraEnvID))
if err != nil {
return nil, fmt.Errorf("Failed to list hosts: %w", errorutil.GetAssistedError(err))
}

// Load fencing credentials from single file ONCE before processing hosts
fencingCreds, err := loadFencingCredentials(getHostConfigDir())
if err != nil {
return nil, fmt.Errorf("Failed to load fencing credentials: %w", err)
}

failures := []Failure{}

for _, host := range hostList.Payload {
// Apply MAC-based configuration first (role, disk hints)
if err := applyHostConfig(ctx, log, bmInventory, host, hostConfigs); err != nil {
if fail, ok := err.(Failure); ok {
failures = append(failures, fail)
Expand All @@ -48,6 +130,16 @@ func ApplyHostConfigs(ctx context.Context, log *log.Logger, bmInventory *client.
return failures, err
}
}

// Apply hostname-based configuration (fencing credentials)
if err := applyHostConfigByHostname(ctx, log, bmInventory, host, fencingCreds); err != nil {
if fail, ok := err.(Failure); ok {
failures = append(failures, fail)
log.Error(err.Error())
} else {
return failures, err
}
}
}

missing := hostConfigs.missing(log)
Expand Down Expand Up @@ -125,6 +217,70 @@ func applyHostConfig(ctx context.Context, log *log.Logger, bmInventory *client.A
return nil
}

// applyHostConfigByHostname applies configuration to a host by matching on hostname
// instead of MAC address. This is used for configurations where hostname is the key,
// such as fencing credentials.
//
// This function is called AFTER applyHostConfig() to ensure MAC-based configurations
// (role, disk hints) are applied first.
func applyHostConfigByHostname(ctx context.Context, log *log.Logger, bmInventory *client.AssistedInstall, host *models.Host, fencingCreds map[string]*models.FencingCredentialsParams) error {
log.Infof("Checking hostname-based configuration for host %s", *host.ID)

// If no fencing credentials loaded, skip
if fencingCreds == nil {
log.Info("No fencing credentials available")
return nil
}

if len(host.Inventory) == 0 {
log.Info("Inventory information not yet available")
return nil
}

inventory := &models.Inventory{}
err := inventory.UnmarshalBinary([]byte(host.Inventory))
if err != nil {
return fmt.Errorf("failed to unmarshal host inventory: %w", err)
}

if inventory.Hostname == "" {
log.Info("Host has no hostname, skipping hostname-based configuration")
return nil
}

// Lookup fencing credentials by hostname in the map
fc, exists := fencingCreds[inventory.Hostname]
if !exists {
log.Infof("No fencing credentials found for hostname %s", inventory.Hostname)
return nil
}

log.Infof("Found fencing credentials for hostname %s", inventory.Hostname)

updateParams := &models.HostUpdateParams{
FencingCredentials: fc,
}

log.Info("Updating host with fencing credentials")
params := installer.NewV2UpdateHostParams().
WithHostID(*host.ID).
WithInfraEnvID(host.InfraEnvID).
WithHostUpdateParams(updateParams)
_, err = bmInventory.Installer.V2UpdateHost(ctx, params)
if err != nil {
if errorResponse, ok := err.(errorutil.AssistedServiceErrorAPI); ok {
return &UpdateFailure{
response: errorResponse,
params: updateParams,
host: host,
inventory: inventory,
}
}
return fmt.Errorf("failed to update Host: %w", err)
}
return nil
}

func applyRootDeviceHints(log *log.Logger, host *models.Host, inventory *models.Inventory, rdh *bmh_v1alpha1.RootDeviceHints, updateParams *models.HostUpdateParams) bool {
acceptableDisks := hostutil.GetAcceptableDisksWithHints(inventory.Disks, rdh)
if host.InstallationDiskID != "" {
Expand Down
Loading