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  }