github.com/openshift/installer@v1.4.17/pkg/asset/agent/manifests/nmstateconfig.go (about) 1 package manifests 2 3 import ( 4 "bytes" 5 "context" 6 "fmt" 7 "io" 8 "net" 9 "os" 10 "path/filepath" 11 12 "github.com/pkg/errors" 13 "github.com/sirupsen/logrus" 14 corev1 "k8s.io/api/core/v1" 15 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 16 "k8s.io/apimachinery/pkg/util/validation/field" 17 "k8s.io/apimachinery/pkg/util/yaml" 18 k8syaml "sigs.k8s.io/yaml" 19 20 aiv1beta1 "github.com/openshift/assisted-service/api/v1beta1" 21 "github.com/openshift/assisted-service/models" 22 "github.com/openshift/installer/pkg/asset" 23 "github.com/openshift/installer/pkg/asset/agent" 24 "github.com/openshift/installer/pkg/asset/agent/agentconfig" 25 "github.com/openshift/installer/pkg/asset/agent/joiner" 26 "github.com/openshift/installer/pkg/asset/agent/manifests/staticnetworkconfig" 27 "github.com/openshift/installer/pkg/asset/agent/workflow" 28 "github.com/openshift/installer/pkg/types" 29 agenttype "github.com/openshift/installer/pkg/types/agent" 30 ) 31 32 var ( 33 nmStateConfigFilename = filepath.Join(clusterManifestDir, "nmstateconfig.yaml") 34 ) 35 36 // NMStateConfig generates the nmstateconfig.yaml file. 37 type NMStateConfig struct { 38 File *asset.File 39 StaticNetworkConfig []*models.HostStaticNetworkConfig 40 Config []*aiv1beta1.NMStateConfig 41 } 42 43 type nmStateConfig struct { 44 Interfaces []struct { 45 IPV4 struct { 46 Address []struct { 47 IP string `yaml:"ip,omitempty"` 48 } `yaml:"address,omitempty"` 49 } `yaml:"ipv4,omitempty"` 50 IPV6 struct { 51 Address []struct { 52 IP string `yaml:"ip,omitempty"` 53 } `yaml:"address,omitempty"` 54 } `yaml:"ipv6,omitempty"` 55 } `yaml:"interfaces,omitempty"` 56 } 57 58 var _ asset.WritableAsset = (*NMStateConfig)(nil) 59 60 // Name returns a human friendly name for the asset. 61 func (*NMStateConfig) Name() string { 62 return "NMState Config" 63 } 64 65 // Dependencies returns all of the dependencies directly needed to generate 66 // the asset. 67 func (*NMStateConfig) Dependencies() []asset.Asset { 68 return []asset.Asset{ 69 &workflow.AgentWorkflow{}, 70 &joiner.ClusterInfo{}, 71 &agentconfig.AgentHosts{}, 72 &agent.OptionalInstallConfig{}, 73 } 74 } 75 76 // Generate generates the NMStateConfig manifest. 77 func (n *NMStateConfig) Generate(_ context.Context, dependencies asset.Parents) error { 78 agentWorkflow := &workflow.AgentWorkflow{} 79 clusterInfo := &joiner.ClusterInfo{} 80 agentHosts := &agentconfig.AgentHosts{} 81 installConfig := &agent.OptionalInstallConfig{} 82 dependencies.Get(agentHosts, installConfig, agentWorkflow, clusterInfo) 83 84 staticNetworkConfig := []*models.HostStaticNetworkConfig{} 85 nmStateConfigs := []*aiv1beta1.NMStateConfig{} 86 var data string 87 var isNetworkConfigAvailable bool 88 var clusterName, clusterNamespace string 89 90 if len(agentHosts.Hosts) == 0 { 91 return nil 92 } 93 94 switch agentWorkflow.Workflow { 95 case workflow.AgentWorkflowTypeInstall: 96 if err := validateHostCount(installConfig.Config, agentHosts); err != nil { 97 return err 98 } 99 clusterName = installConfig.ClusterName() 100 clusterNamespace = installConfig.ClusterNamespace() 101 102 case workflow.AgentWorkflowTypeAddNodes: 103 if err := validateHostHostnameAndIPs(agentHosts, clusterInfo.Nodes); err != nil { 104 return err 105 } 106 clusterName = clusterInfo.ClusterName 107 clusterNamespace = clusterInfo.Namespace 108 109 default: 110 return fmt.Errorf("AgentWorkflowType value not supported: %s", agentWorkflow.Workflow) 111 } 112 113 for i, host := range agentHosts.Hosts { 114 if host.NetworkConfig.Raw != nil { 115 isNetworkConfigAvailable = true 116 117 nmStateConfig := aiv1beta1.NMStateConfig{ 118 TypeMeta: metav1.TypeMeta{ 119 Kind: "NMStateConfig", 120 APIVersion: aiv1beta1.GroupVersion.String(), 121 }, 122 ObjectMeta: metav1.ObjectMeta{ 123 Name: fmt.Sprintf("%s-%d", clusterName, i), 124 Namespace: clusterNamespace, 125 Labels: getNMStateConfigLabels(clusterName), 126 }, 127 Spec: aiv1beta1.NMStateConfigSpec{ 128 NetConfig: aiv1beta1.NetConfig{ 129 Raw: []byte(host.NetworkConfig.Raw), 130 }, 131 }, 132 } 133 for _, hostInterface := range host.Interfaces { 134 intrfc := aiv1beta1.Interface{ 135 Name: hostInterface.Name, 136 MacAddress: hostInterface.MacAddress, 137 } 138 nmStateConfig.Spec.Interfaces = append(nmStateConfig.Spec.Interfaces, &intrfc) 139 140 } 141 nmStateConfigs = append(nmStateConfigs, &nmStateConfig) 142 143 staticNetworkConfig = append(staticNetworkConfig, &models.HostStaticNetworkConfig{ 144 MacInterfaceMap: buildMacInterfaceMap(nmStateConfig), 145 NetworkYaml: string(nmStateConfig.Spec.NetConfig.Raw), 146 }) 147 148 // Marshal the nmStateConfig one at a time 149 // and add a yaml separator with new line 150 // so as not to marshal the nmStateConfigs 151 // as a yaml list in the generated nmstateconfig.yaml 152 nmStateConfigData, err := k8syaml.Marshal(nmStateConfig) 153 154 if err != nil { 155 return errors.Wrap(err, "failed to marshal agent installer NMStateConfig") 156 } 157 data = fmt.Sprint(data, fmt.Sprint(string(nmStateConfigData), "---\n")) 158 } 159 } 160 161 if isNetworkConfigAvailable { 162 n.Config = nmStateConfigs 163 n.StaticNetworkConfig = staticNetworkConfig 164 165 n.File = &asset.File{ 166 Filename: nmStateConfigFilename, 167 Data: []byte(data), 168 } 169 } 170 return n.finish() 171 } 172 173 // Files returns the files generated by the asset. 174 func (n *NMStateConfig) Files() []*asset.File { 175 if n.File != nil { 176 return []*asset.File{n.File} 177 } 178 return []*asset.File{} 179 } 180 181 // Load returns the NMStateConfig asset from the disk. 182 func (n *NMStateConfig) Load(f asset.FileFetcher) (bool, error) { 183 184 file, err := f.FetchByName(nmStateConfigFilename) 185 if err != nil { 186 if os.IsNotExist(err) { 187 return false, nil 188 } 189 return false, errors.Wrapf(err, "failed to load file %s", nmStateConfigFilename) 190 } 191 192 // Split up the file into multiple YAMLs if it contains NMStateConfig for more than one node 193 yamlList, err := GetMultipleYamls[aiv1beta1.NMStateConfig](file.Data) 194 if err != nil { 195 return false, errors.Wrapf(err, "could not decode YAML for %s", nmStateConfigFilename) 196 } 197 198 var staticNetworkConfig []*models.HostStaticNetworkConfig 199 var nmStateConfigList []*aiv1beta1.NMStateConfig 200 201 for i := range yamlList { 202 nmStateConfig := yamlList[i] 203 staticNetworkConfig = append(staticNetworkConfig, &models.HostStaticNetworkConfig{ 204 MacInterfaceMap: buildMacInterfaceMap(nmStateConfig), 205 NetworkYaml: string(nmStateConfig.Spec.NetConfig.Raw), 206 }) 207 nmStateConfigList = append(nmStateConfigList, &nmStateConfig) 208 } 209 210 n.File, n.StaticNetworkConfig, n.Config = file, staticNetworkConfig, nmStateConfigList 211 if err = n.finish(); err != nil { 212 return false, err 213 } 214 return true, nil 215 } 216 217 func (n *NMStateConfig) finish() error { 218 219 if err := n.validateWithNMStateCtl(); err != nil { 220 return err 221 } 222 223 if errList := n.validateNMStateConfig().ToAggregate(); errList != nil { 224 return errors.Wrapf(errList, "invalid NMStateConfig configuration") 225 } 226 return nil 227 } 228 229 func (n *NMStateConfig) validateWithNMStateCtl() error { 230 level := logrus.GetLevel() 231 logrus.SetLevel(logrus.WarnLevel) 232 staticNetworkConfigGenerator := staticnetworkconfig.New(logrus.WithField("pkg", "manifests"), staticnetworkconfig.Config{MaxConcurrentGenerations: 2}) 233 defer logrus.SetLevel(level) 234 235 // Validate the network config using nmstatectl 236 if err := staticNetworkConfigGenerator.ValidateStaticConfigParams(context.Background(), n.StaticNetworkConfig); err != nil { 237 return errors.Wrapf(err, "staticNetwork configuration is not valid") 238 } 239 return nil 240 } 241 242 func (n *NMStateConfig) validateNMStateConfig() field.ErrorList { 243 allErrs := field.ErrorList{} 244 245 if err := n.validateNMStateLabels(); err != nil { 246 allErrs = append(allErrs, err...) 247 } 248 249 return allErrs 250 } 251 252 func (n *NMStateConfig) validateNMStateLabels() field.ErrorList { 253 254 var allErrs field.ErrorList 255 256 fieldPath := field.NewPath("ObjectMeta", "Labels") 257 258 for _, nmStateConfig := range n.Config { 259 if len(nmStateConfig.ObjectMeta.Labels) == 0 { 260 allErrs = append(allErrs, field.Required(fieldPath, fmt.Sprintf("%s does not have any label set", nmStateConfig.Name))) 261 } 262 } 263 264 return allErrs 265 } 266 267 func getFirstIP(nmstateRaw []byte) (string, error) { 268 var nmStateConfig nmStateConfig 269 err := yaml.Unmarshal(nmstateRaw, &nmStateConfig) 270 if err != nil { 271 return "", fmt.Errorf("error unmarshalling NMStateConfig: %w", err) 272 } 273 274 for _, intf := range nmStateConfig.Interfaces { 275 for _, addr4 := range intf.IPV4.Address { 276 if addr4.IP != "" { 277 return addr4.IP, nil 278 } 279 } 280 for _, addr6 := range intf.IPV6.Address { 281 if addr6.IP != "" { 282 return addr6.IP, nil 283 } 284 } 285 } 286 287 return "", nil 288 } 289 290 // GetNodeZeroIP retrieves the first IP to be set as the node0 IP. 291 // The method prioritizes the search by trying to scan first the NMState configs defined 292 // in the agent-config hosts - so that it would be possible to skip the worker nodes - and then 293 // the NMStateConfig. 294 func GetNodeZeroIP(hosts []agenttype.Host, nmStateConfigs []*aiv1beta1.NMStateConfig) (string, error) { 295 rawConfigs := []aiv1beta1.RawNetConfig{} 296 297 // Select first the configs from the hosts, if defined 298 // Skip worker hosts (or without an explicit role assigned) 299 for _, host := range hosts { 300 if host.Role != "master" { 301 continue 302 } 303 rawConfigs = append(rawConfigs, host.NetworkConfig.Raw) 304 } 305 306 // Add other hosts without explicit role with a lower 307 // priority as potential candidates 308 for _, host := range hosts { 309 if host.Role != "" { 310 continue 311 } 312 rawConfigs = append(rawConfigs, host.NetworkConfig.Raw) 313 } 314 315 // Fallback on nmstate configs (in case hosts weren't found or didn't have static configuration) 316 for _, nmStateConfig := range nmStateConfigs { 317 rawConfigs = append(rawConfigs, nmStateConfig.Spec.NetConfig.Raw) 318 } 319 320 // Try to look for an eligible IP 321 for _, raw := range rawConfigs { 322 nodeZeroIP, err := getFirstIP(raw) 323 if err != nil { 324 return "", fmt.Errorf("error unmarshalling NMStateConfig: %w", err) 325 } 326 if nodeZeroIP == "" { 327 continue 328 } 329 if net.ParseIP(nodeZeroIP) == nil { 330 return "", fmt.Errorf("could not parse static IP: %s", nodeZeroIP) 331 } 332 return nodeZeroIP, nil 333 } 334 335 return "", fmt.Errorf("invalid NMState configurations provided, no interface IPs set") 336 } 337 338 // GetNMIgnitionFiles returns the list of NetworkManager configuration files 339 func GetNMIgnitionFiles(staticNetworkConfig []*models.HostStaticNetworkConfig) ([]staticnetworkconfig.StaticNetworkConfigData, error) { 340 341 level := logrus.GetLevel() 342 logrus.SetLevel(logrus.WarnLevel) 343 staticNetworkConfigGenerator := staticnetworkconfig.New(logrus.WithField("pkg", "manifests"), staticnetworkconfig.Config{MaxConcurrentGenerations: 2}) 344 defer logrus.SetLevel(level) 345 346 networkConfigStr, err := staticNetworkConfigGenerator.FormatStaticNetworkConfigForDB(staticNetworkConfig) 347 if err != nil { 348 err = fmt.Errorf("error marshalling StaticNetwork configuration: %w", err) 349 return nil, err 350 } 351 352 filesList, err := staticNetworkConfigGenerator.GenerateStaticNetworkConfigData(context.Background(), networkConfigStr) 353 if err != nil { 354 err = fmt.Errorf("failed to create StaticNetwork config data: %w", err) 355 return nil, err 356 } 357 358 return filesList, err 359 } 360 361 // GetMultipleYamls reads a YAML file containing multiple YAML definitions of the same format 362 // Each specific format must be of type DecodeFormat 363 func GetMultipleYamls[T any](contents []byte) ([]T, error) { 364 365 r := bytes.NewReader(contents) 366 dec := yaml.NewYAMLToJSONDecoder(r) 367 368 var outputList []T 369 for { 370 decodedData := new(T) 371 err := dec.Decode(&decodedData) 372 if errors.Is(err, io.EOF) { 373 break 374 } 375 if err != nil { 376 return nil, errors.Wrapf(err, "Error reading multiple YAMLs") 377 } 378 379 if decodedData != nil { 380 outputList = append(outputList, *decodedData) 381 } 382 } 383 384 return outputList, nil 385 } 386 387 func buildMacInterfaceMap(nmStateConfig aiv1beta1.NMStateConfig) models.MacInterfaceMap { 388 389 // TODO - this eventually will move to another asset so the interface definition can be shared with Butane 390 macInterfaceMap := make(models.MacInterfaceMap, 0, len(nmStateConfig.Spec.Interfaces)) 391 for _, cfg := range nmStateConfig.Spec.Interfaces { 392 logrus.Debug("adding MAC interface map to host static network config - Name: ", cfg.Name, " MacAddress:", cfg.MacAddress) 393 macInterfaceMap = append(macInterfaceMap, &models.MacInterfaceMapItems0{ 394 MacAddress: cfg.MacAddress, 395 LogicalNicName: cfg.Name, 396 }) 397 } 398 return macInterfaceMap 399 } 400 401 func validateHostCount(installConfig *types.InstallConfig, agentHosts *agentconfig.AgentHosts) error { 402 numRequiredMasters, numRequiredWorkers := agent.GetReplicaCount(installConfig) 403 404 numMasters := int64(0) 405 numWorkers := int64(0) 406 // Check for hosts explicitly defined 407 for _, host := range agentHosts.Hosts { 408 switch host.Role { 409 case "master": 410 numMasters++ 411 case "worker": 412 numWorkers++ 413 } 414 } 415 416 // If role is not defined it will first be assigned as a master 417 for _, host := range agentHosts.Hosts { 418 if host.Role == "" { 419 if numMasters < numRequiredMasters { 420 numMasters++ 421 } else { 422 numWorkers++ 423 } 424 } 425 } 426 427 if numMasters != 0 && numMasters < numRequiredMasters { 428 logrus.Warnf("not enough master hosts defined (%v) to support all the configured ControlPlane replicas (%v)", numMasters, numRequiredMasters) 429 } 430 if numMasters > numRequiredMasters { 431 return fmt.Errorf("the number of master hosts defined (%v) exceeds the configured ControlPlane replicas (%v)", numMasters, numRequiredMasters) 432 } 433 434 if numWorkers != 0 && numWorkers < numRequiredWorkers { 435 logrus.Warnf("not enough worker hosts defined (%v) to support all the configured Compute replicas (%v)", numWorkers, numRequiredWorkers) 436 } 437 if numWorkers > numRequiredWorkers { 438 return fmt.Errorf("the number of worker hosts defined (%v) exceeds the configured Compute replicas (%v)", numWorkers, numRequiredWorkers) 439 } 440 441 return nil 442 } 443 444 func validateHostHostnameAndIPs(agentHosts *agentconfig.AgentHosts, nodes *corev1.NodeList) error { 445 for _, host := range agentHosts.Hosts { 446 hostIPs, err := getAllHostIPs(host.NetworkConfig) 447 if err != nil { 448 return err 449 } 450 451 for _, node := range nodes.Items { 452 for _, addr := range node.Status.Addresses { 453 if _, found := hostIPs[addr.Address]; found { 454 return fmt.Errorf("address conflict found. The configured address %s is already used by the cluster node %s", addr.Address, node.GetName()) 455 } 456 if host.Hostname != "" && host.Hostname == addr.Address { 457 return fmt.Errorf("hostname conflict found. The configured hostname %s is already used in the cluster", addr.Address) 458 } 459 } 460 } 461 } 462 return nil 463 } 464 465 func getAllHostIPs(config aiv1beta1.NetConfig) (map[string]struct{}, error) { 466 var nmStateConfig nmStateConfig 467 hostIPs := make(map[string]struct{}) 468 469 err := yaml.Unmarshal(config.Raw, &nmStateConfig) 470 if err != nil { 471 return hostIPs, fmt.Errorf("error unmarshalling NMStateConfig: %w", err) 472 } 473 474 for _, intf := range nmStateConfig.Interfaces { 475 for _, addr4 := range intf.IPV4.Address { 476 if addr4.IP != "" { 477 hostIPs[addr4.IP] = struct{}{} 478 } 479 } 480 for _, addr6 := range intf.IPV6.Address { 481 if addr6.IP != "" { 482 hostIPs[addr6.IP] = struct{}{} 483 } 484 } 485 } 486 return hostIPs, nil 487 }