github.com/openshift/installer@v1.4.17/pkg/asset/agent/agentconfig/agenthosts.go (about)

     1  package agentconfig
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"path/filepath"
     7  	"strings"
     8  
     9  	"github.com/pkg/errors"
    10  	"github.com/sirupsen/logrus"
    11  	"k8s.io/apimachinery/pkg/util/validation/field"
    12  	"sigs.k8s.io/yaml"
    13  
    14  	aiv1beta1 "github.com/openshift/assisted-service/api/v1beta1"
    15  	"github.com/openshift/installer/pkg/asset"
    16  	agentAsset "github.com/openshift/installer/pkg/asset/agent"
    17  	"github.com/openshift/installer/pkg/asset/agent/joiner"
    18  	"github.com/openshift/installer/pkg/asset/agent/workflow"
    19  	"github.com/openshift/installer/pkg/types/agent"
    20  	"github.com/openshift/installer/pkg/types/baremetal/validation"
    21  	"github.com/openshift/installer/pkg/validate"
    22  )
    23  
    24  var (
    25  	_ asset.WritableAsset = (*AgentHosts)(nil)
    26  )
    27  
    28  const (
    29  	masterRole string = "master"
    30  	workerRole string = "worker"
    31  )
    32  
    33  type nmStateInterface struct {
    34  	Interfaces []struct {
    35  		MACAddress string `json:"mac-address,omitempty"`
    36  		Name       string `json:"name,omitempty"`
    37  	} `yaml:"interfaces,omitempty"`
    38  }
    39  
    40  // AgentHosts generates the hosts information from the AgentConfig and
    41  // OptionalInstallConfig assets.
    42  type AgentHosts struct {
    43  	Hosts        []agent.Host
    44  	rendezvousIP string
    45  }
    46  
    47  // Name returns a human friendly name.
    48  func (a *AgentHosts) Name() string {
    49  	return "Agent Hosts"
    50  }
    51  
    52  // Dependencies returns all of the dependencies directly needed the asset.
    53  func (a *AgentHosts) Dependencies() []asset.Asset {
    54  	return []asset.Asset{
    55  		&workflow.AgentWorkflow{},
    56  		&joiner.AddNodesConfig{},
    57  		&agentAsset.OptionalInstallConfig{},
    58  		&AgentConfig{},
    59  	}
    60  }
    61  
    62  // Generate generates the Hosts data.
    63  func (a *AgentHosts) Generate(_ context.Context, dependencies asset.Parents) error {
    64  	agentWorkflow := &workflow.AgentWorkflow{}
    65  	addNodesConfig := &joiner.AddNodesConfig{}
    66  	agentConfig := &AgentConfig{}
    67  	installConfig := &agentAsset.OptionalInstallConfig{}
    68  	dependencies.Get(agentConfig, installConfig, agentWorkflow, addNodesConfig)
    69  
    70  	switch agentWorkflow.Workflow {
    71  	case workflow.AgentWorkflowTypeInstall:
    72  		if agentConfig.Config != nil {
    73  			a.rendezvousIP = agentConfig.Config.RendezvousIP
    74  			a.Hosts = append(a.Hosts, agentConfig.Config.Hosts...)
    75  			if len(a.Hosts) > 0 {
    76  				// Hosts defined in agent-config take precedence
    77  				logrus.Debugf("Using hosts from %s", agentConfigFilename)
    78  			}
    79  		}
    80  
    81  		if installConfig != nil && installConfig.GetBaremetalHosts() != nil {
    82  			// Only use hosts from install-config if they are not defined in agent-config
    83  			if len(a.Hosts) == 0 {
    84  				if err := a.getInstallConfigDefaults(installConfig); err != nil {
    85  					return errors.Wrapf(err, "invalid host definition in %s", agentAsset.InstallConfigFilename)
    86  				}
    87  			} else {
    88  				logrus.Warnf(fmt.Sprintf("hosts from %s are ignored", agentAsset.InstallConfigFilename))
    89  			}
    90  		}
    91  
    92  	case workflow.AgentWorkflowTypeAddNodes:
    93  		a.Hosts = append(a.Hosts, addNodesConfig.Config.Hosts...)
    94  
    95  	default:
    96  		return fmt.Errorf("AgentWorkflowType value not supported: %s", agentWorkflow.Workflow)
    97  	}
    98  
    99  	if err := a.validateAgentHosts().ToAggregate(); err != nil {
   100  		return errors.Wrapf(err, "invalid Hosts configuration")
   101  	}
   102  
   103  	return nil
   104  }
   105  
   106  // Files returns the files generated by the asset.
   107  func (a *AgentHosts) Files() []*asset.File {
   108  	return nil
   109  }
   110  
   111  // Load currently does nothing.
   112  func (a *AgentHosts) Load(f asset.FileFetcher) (bool, error) {
   113  	return false, nil
   114  }
   115  
   116  func (a *AgentHosts) validateAgentHosts() field.ErrorList {
   117  	allErrs := field.ErrorList{}
   118  
   119  	macs := make(map[string]bool)
   120  	for i, host := range a.Hosts {
   121  		hostPath := field.NewPath("Hosts").Index(i)
   122  
   123  		if err := a.validateHostInterfaces(hostPath, host, macs); err != nil {
   124  			allErrs = append(allErrs, err...)
   125  		}
   126  
   127  		if err := a.validateHostRootDeviceHints(hostPath, host); err != nil {
   128  			allErrs = append(allErrs, err...)
   129  		}
   130  
   131  		if err := a.validateRoles(hostPath, host); err != nil {
   132  			allErrs = append(allErrs, err...)
   133  		}
   134  	}
   135  
   136  	if err := a.validateRendezvousIPNotWorker(a.rendezvousIP, a.Hosts); err != nil {
   137  		allErrs = append(allErrs, err...)
   138  	}
   139  
   140  	return allErrs
   141  }
   142  
   143  func (a *AgentHosts) validateHostInterfaces(hostPath *field.Path, host agent.Host, macs map[string]bool) field.ErrorList {
   144  	var allErrs field.ErrorList
   145  
   146  	interfacePath := hostPath.Child("Interfaces")
   147  	if len(host.Interfaces) == 0 {
   148  		allErrs = append(allErrs, field.Required(interfacePath, "at least one interface must be defined for each node"))
   149  	}
   150  
   151  	for j := range host.Interfaces {
   152  		mac := host.Interfaces[j].MacAddress
   153  		macAddressPath := interfacePath.Index(j).Child("macAddress")
   154  
   155  		if mac == "" {
   156  			allErrs = append(allErrs, field.Required(macAddressPath, "each interface must have a MAC address defined"))
   157  			continue
   158  		}
   159  
   160  		if err := validate.MAC(mac); err != nil {
   161  			allErrs = append(allErrs, field.Invalid(macAddressPath, mac, err.Error()))
   162  		}
   163  
   164  		if _, ok := macs[mac]; ok {
   165  			allErrs = append(allErrs, field.Invalid(macAddressPath, mac, "duplicate MAC address found"))
   166  		}
   167  		macs[mac] = true
   168  	}
   169  
   170  	return allErrs
   171  }
   172  
   173  func (a *AgentHosts) validateHostRootDeviceHints(hostPath *field.Path, host agent.Host) field.ErrorList {
   174  	rdhPath := hostPath.Child("rootDeviceHints")
   175  	allErrs := validation.ValidateHostRootDeviceHints(&host.RootDeviceHints, rdhPath)
   176  
   177  	if host.RootDeviceHints.WWNWithExtension != "" {
   178  		allErrs = append(allErrs, field.Forbidden(
   179  			rdhPath.Child("wwnWithExtension"), "WWN extensions are not supported in root device hints"))
   180  	}
   181  
   182  	if host.RootDeviceHints.WWNVendorExtension != "" {
   183  		allErrs = append(allErrs, field.Forbidden(rdhPath.Child("wwnVendorExtension"), "WWN vendor extensions are not supported in root device hints"))
   184  	}
   185  
   186  	return allErrs
   187  }
   188  
   189  func (a *AgentHosts) validateRoles(hostPath *field.Path, host agent.Host) field.ErrorList {
   190  	var allErrs field.ErrorList
   191  
   192  	if len(host.Role) > 0 && host.Role != masterRole && host.Role != workerRole {
   193  		allErrs = append(allErrs, field.Forbidden(hostPath.Child("Host"), "host role has incorrect value. Role must either be 'master' or 'worker'"))
   194  	}
   195  
   196  	return allErrs
   197  }
   198  
   199  func (a *AgentHosts) validateRendezvousIPNotWorker(rendezvousIP string, hosts []agent.Host) field.ErrorList {
   200  	var allErrs field.ErrorList
   201  
   202  	if rendezvousIP != "" {
   203  		for i, host := range hosts {
   204  			hostPath := field.NewPath("Hosts").Index(i)
   205  			if strings.Contains(string(host.NetworkConfig.Raw), rendezvousIP) && host.Role == workerRole {
   206  				errMsg := "Host " + host.Hostname + " has role 'worker' and has the rendezvousIP assigned to it. The rendezvousIP must be assigned to a control plane host."
   207  				allErrs = append(allErrs, field.Forbidden(hostPath.Child("Host"), errMsg))
   208  			}
   209  		}
   210  	}
   211  	return allErrs
   212  }
   213  
   214  // Add the baremetal hosts defined in install-config to the agent Hosts.
   215  func (a *AgentHosts) getInstallConfigDefaults(installConfig *agentAsset.OptionalInstallConfig) error {
   216  	for _, icHost := range installConfig.GetBaremetalHosts() {
   217  		if icHost.BootMACAddress == "" {
   218  			return errors.New("host bootMACAddress is required")
   219  		}
   220  
   221  		host := agent.Host{
   222  			Hostname: icHost.Name,
   223  			Role:     icHost.Role,
   224  		}
   225  		if icHost.RootDeviceHints != nil {
   226  			host.RootDeviceHints = *icHost.RootDeviceHints
   227  		}
   228  		if icHost.NetworkConfig != nil {
   229  			contents, err := yaml.JSONToYAML(icHost.NetworkConfig.Raw)
   230  			if err != nil {
   231  				return errors.Wrap(err, "failed to unmarshal networkConfig")
   232  			}
   233  			host.NetworkConfig.Raw = contents
   234  
   235  			// Create interfaces table from NetworkConfig
   236  			var netInterfaces nmStateInterface
   237  			err = yaml.Unmarshal(contents, &netInterfaces)
   238  			if err != nil {
   239  				return fmt.Errorf("error unmarshalling NMStateConfig: %w", err)
   240  			}
   241  
   242  			var foundBootMac = false
   243  			for _, intf := range netInterfaces.Interfaces {
   244  				if intf.Name != "" && intf.MACAddress != "" {
   245  					hostInterface := &aiv1beta1.Interface{
   246  						Name:       intf.Name,
   247  						MacAddress: intf.MACAddress,
   248  					}
   249  					host.Interfaces = append(host.Interfaces, hostInterface)
   250  					if icHost.BootMACAddress == intf.MACAddress {
   251  						foundBootMac = true
   252  					}
   253  				}
   254  			}
   255  
   256  			if !foundBootMac {
   257  				logrus.Warnf("For host %s, BootMACAddress %s is not in NetworkConfig", icHost.Name, icHost.BootMACAddress)
   258  			}
   259  		}
   260  		if len(host.Interfaces) == 0 {
   261  			// Create interfaces table from BootMacAddress
   262  			hostInterface := &aiv1beta1.Interface{
   263  				Name:       "boot",
   264  				MacAddress: icHost.BootMACAddress,
   265  			}
   266  			host.Interfaces = append(host.Interfaces, hostInterface)
   267  		}
   268  
   269  		// Add BMC configuration
   270  		host.BMC = icHost.BMC
   271  
   272  		logrus.Debugf("Using host %s from %s", host.Hostname, agentAsset.InstallConfigFilename)
   273  		a.Hosts = append(a.Hosts, host)
   274  	}
   275  	return nil
   276  }
   277  
   278  // HostConfigFileMap is a map from a filepath ("<host>/<file>") to file content
   279  // for hostconfig files.
   280  type HostConfigFileMap map[string][]byte
   281  
   282  // HostConfigFiles returns a map from filename to contents of the files used for
   283  // host-specific configuration by the agent installer client.
   284  func (a *AgentHosts) HostConfigFiles() (HostConfigFileMap, error) {
   285  	if a == nil {
   286  		return nil, nil
   287  	}
   288  
   289  	files := HostConfigFileMap{}
   290  	for i, host := range a.Hosts {
   291  		name := fmt.Sprintf("host-%d", i)
   292  		if host.Hostname != "" {
   293  			name = host.Hostname
   294  		}
   295  
   296  		macs := []string{}
   297  		for _, iface := range host.Interfaces {
   298  			macs = append(macs, strings.ToLower(iface.MacAddress)+"\n")
   299  		}
   300  
   301  		if len(macs) > 0 {
   302  			files[filepath.Join(name, "mac_addresses")] = []byte(strings.Join(macs, ""))
   303  		}
   304  
   305  		rdh, err := yaml.Marshal(host.RootDeviceHints)
   306  		if err != nil {
   307  			return nil, err
   308  		}
   309  		if len(rdh) > 0 && string(rdh) != "{}\n" {
   310  			files[filepath.Join(name, "root-device-hints.yaml")] = rdh
   311  		}
   312  
   313  		if len(host.Role) > 0 {
   314  			files[filepath.Join(name, "role")] = []byte(host.Role)
   315  		}
   316  	}
   317  	return files, nil
   318  }