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 }