github.com/openshift/installer@v1.4.17/pkg/asset/agent/manifests/staticnetworkconfig/generator.go (about)

     1  package staticnetworkconfig
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"fmt"
     7  	"os"
     8  	"os/exec"
     9  	"path/filepath"
    10  	"sort"
    11  	"strings"
    12  
    13  	"github.com/hashicorp/go-multierror"
    14  	"github.com/pkg/errors"
    15  	"github.com/sirupsen/logrus"
    16  	"golang.org/x/sync/semaphore"
    17  	"gopkg.in/ini.v1"
    18  	"gopkg.in/yaml.v2"
    19  	"k8s.io/apimachinery/pkg/util/json"
    20  
    21  	"github.com/openshift/assisted-service/models"
    22  )
    23  
    24  // Config is the configuration for the nmstatectl runner.
    25  type Config struct {
    26  	MaxConcurrentGenerations int64 `envconfig:"MAX_CONCURRENT_NMSTATECTL_GENERATIONS" default:"30"`
    27  }
    28  
    29  // StaticNetworkConfigData represents a NetworkManager keyfile.
    30  type StaticNetworkConfigData struct { //nolint:revive
    31  	FilePath     string
    32  	FileContents string
    33  }
    34  
    35  // StaticNetworkConfig is the interface for converting NMState.
    36  type StaticNetworkConfig interface {
    37  	GenerateStaticNetworkConfigData(ctx context.Context, hostsYAMLS string) ([]StaticNetworkConfigData, error)
    38  	FormatStaticNetworkConfigForDB(staticNetworkConfig []*models.HostStaticNetworkConfig) (string, error)
    39  	ValidateStaticConfigParams(ctx context.Context, staticNetworkConfig []*models.HostStaticNetworkConfig) error
    40  	ValidateNMStateYaml(ctx context.Context, networkYaml string) error
    41  }
    42  
    43  type staticNetworkConfigGenerator struct {
    44  	Config
    45  	log logrus.FieldLogger
    46  	sem *semaphore.Weighted
    47  }
    48  
    49  // New returns a new network config generator.
    50  func New(log logrus.FieldLogger, cfg Config) StaticNetworkConfig {
    51  	return &staticNetworkConfigGenerator{
    52  		Config: cfg,
    53  		log:    log,
    54  		sem:    semaphore.NewWeighted(cfg.MaxConcurrentGenerations)}
    55  }
    56  
    57  // GenerateStaticNetworkConfigData converts the NMState config to NetworkManager key files.
    58  func (s *staticNetworkConfigGenerator) GenerateStaticNetworkConfigData(ctx context.Context, staticNetworkConfigStr string) ([]StaticNetworkConfigData, error) {
    59  	staticNetworkConfig, err := s.decodeStaticNetworkConfig(staticNetworkConfigStr)
    60  	if err != nil {
    61  		s.log.WithError(err).Errorf("Failed to decode static network config")
    62  		return nil, err
    63  	}
    64  	s.log.Infof("Start configuring static network for %d hosts", len(staticNetworkConfig))
    65  	filesList := []StaticNetworkConfigData{}
    66  	for i, hostConfig := range staticNetworkConfig {
    67  		hostFileList, err := s.generateHostStaticNetworkConfigData(ctx, hostConfig, fmt.Sprintf("host%d", i))
    68  		if err != nil {
    69  			err = errors.Wrapf(err, "failed to create static config for host %d", i)
    70  			s.log.Error(err)
    71  			return nil, err
    72  		}
    73  		filesList = append(filesList, hostFileList...)
    74  	}
    75  	return filesList, nil
    76  }
    77  
    78  func (s *staticNetworkConfigGenerator) generateHostStaticNetworkConfigData(ctx context.Context, hostConfig *models.HostStaticNetworkConfig, hostDir string) ([]StaticNetworkConfigData, error) {
    79  	hostYAML := hostConfig.NetworkYaml
    80  	macInterfaceMapping := s.formatMacInterfaceMap(hostConfig.MacInterfaceMap)
    81  	result, err := s.executeNMStatectl(ctx, hostYAML)
    82  	if err != nil {
    83  		return nil, err
    84  	}
    85  	filesList, err := s.createNMConnectionFiles(result, hostDir)
    86  	if err != nil {
    87  		s.log.WithError(err).Errorf("failed to create NM connection files")
    88  		return nil, err
    89  	}
    90  	mapConfigData := StaticNetworkConfigData{
    91  		FilePath:     filepath.Join(hostDir, "mac_interface.ini"),
    92  		FileContents: macInterfaceMapping,
    93  	}
    94  	filesList = append(filesList, mapConfigData)
    95  	return filesList, nil
    96  }
    97  
    98  func (s *staticNetworkConfigGenerator) executeNMStatectl(ctx context.Context, hostYAML string) (string, error) {
    99  	err := s.sem.Acquire(ctx, 1)
   100  	if err != nil {
   101  		s.log.WithError(err).Errorf("Failed to lock semaphore for nmstatectl execution")
   102  		return "", err
   103  	}
   104  	defer s.sem.Release(1)
   105  
   106  	f, err := os.CreateTemp("", "host-config")
   107  	if err != nil {
   108  		s.log.WithError(err).Errorf("Failed to create temp file")
   109  		return "", err
   110  	}
   111  	defer func() {
   112  		f.Close()
   113  		os.Remove(f.Name())
   114  	}()
   115  	_, err = f.WriteString(hostYAML)
   116  	if err != nil {
   117  		s.log.WithError(err).Errorf("Failed to write host config to temp file")
   118  		return "", err
   119  	}
   120  	if err = f.Sync(); err != nil {
   121  		s.log.WithError(err).Warn("Failed to sync file")
   122  	}
   123  	if err = f.Close(); err != nil {
   124  		s.log.WithError(err).Warn("Failed to close file")
   125  	}
   126  
   127  	// Check if nmstatectl executable exists in the system
   128  	nmstatectlPath, err := exec.LookPath("nmstatectl")
   129  	if err != nil {
   130  		return "", fmt.Errorf("install nmstate package, %w", err)
   131  	}
   132  
   133  	var stdoutBytes, stderrBytes bytes.Buffer
   134  	cmd := exec.CommandContext(ctx, nmstatectlPath, "gc", f.Name()) //nolint:gosec
   135  	cmd.Stdout = &stdoutBytes
   136  	cmd.Stderr = &stderrBytes
   137  
   138  	err = cmd.Run()
   139  	if err == nil {
   140  		return stdoutBytes.String(), nil
   141  	}
   142  
   143  	var exitErr *exec.ExitError
   144  	if errors.As(err, &exitErr) {
   145  		s.log.Errorf("<nmstatectl gc> failed, errorCode %d, stderr %s, input yaml <%s>", exitErr.ExitCode(), stderrBytes.String(), hostYAML)
   146  		errMsg := strings.Split(stderrBytes.String(), "Error:")
   147  		return "", fmt.Errorf("failed to execute 'nmstatectl gc', error: %s", strings.TrimSpace(errMsg[len(errMsg)-1]))
   148  	}
   149  	return "", fmt.Errorf("failed to execute 'nmstatectl gc', error: %w", err)
   150  }
   151  
   152  // createNMConnectionFiles formats the nmstate output into a list of file data.
   153  // Nothing is written to the local filesystem.
   154  func (s *staticNetworkConfigGenerator) createNMConnectionFiles(nmstateOutput, hostDir string) ([]StaticNetworkConfigData, error) {
   155  	var hostNMConnections map[string]interface{}
   156  	err := yaml.Unmarshal([]byte(nmstateOutput), &hostNMConnections)
   157  	if err != nil {
   158  		s.log.WithError(err).Errorf("Failed to unmarshal nmstate output")
   159  		return nil, err
   160  	}
   161  	if _, found := hostNMConnections["NetworkManager"]; !found {
   162  		return nil, errors.Errorf("nmstate generated an empty NetworkManager config file content")
   163  	}
   164  	filesList := []StaticNetworkConfigData{}
   165  	connectionsList, ok := hostNMConnections["NetworkManager"].([]interface{})
   166  	if !ok || len(connectionsList) == 0 {
   167  		return nil, errors.Errorf("nmstate generated an empty NetworkManager config file content")
   168  	}
   169  	for _, connection := range connectionsList {
   170  		if connectionElems, ok := connection.([]interface{}); ok {
   171  			if fileName, ok := connectionElems[0].(string); ok {
   172  				if nmConnection, ok := connectionElems[1].(string); ok {
   173  					fileContents, err := s.formatNMConnection(nmConnection)
   174  					if err != nil {
   175  						return nil, err
   176  					}
   177  					s.log.Infof("Adding NMConnection file <%s>", fileName)
   178  					newFile := StaticNetworkConfigData{
   179  						FilePath:     filepath.Join(hostDir, fileName),
   180  						FileContents: fileContents,
   181  					}
   182  					filesList = append(filesList, newFile)
   183  				}
   184  			}
   185  		}
   186  	}
   187  	return filesList, nil
   188  }
   189  
   190  func (s *staticNetworkConfigGenerator) formatNMConnection(nmConnection string) (string, error) {
   191  	ini.PrettyFormat = false
   192  	cfg, err := ini.LoadSources(ini.LoadOptions{IgnoreInlineComment: true}, []byte(nmConnection))
   193  	if err != nil {
   194  		s.log.WithError(err).Errorf("Failed to load the ini format string %s", nmConnection)
   195  		return "", err
   196  	}
   197  	connectionSection := cfg.Section("connection")
   198  	_, err = connectionSection.NewKey("autoconnect", "true")
   199  	if err != nil {
   200  		s.log.WithError(err).Errorf("Failed to add autoconnect key to section connection")
   201  		return "", err
   202  	}
   203  	_, err = connectionSection.NewKey("autoconnect-priority", "1")
   204  	if err != nil {
   205  		s.log.WithError(err).Errorf("Failed to add autoconnect-priority key to section connection")
   206  		return "", err
   207  	}
   208  
   209  	buf := new(bytes.Buffer)
   210  	_, err = cfg.WriteTo(buf)
   211  	if err != nil {
   212  		s.log.WithError(err).Errorf("Failed to output nmconnection ini file to buffer")
   213  		return "", err
   214  	}
   215  	return buf.String(), nil
   216  }
   217  
   218  // ValidateStaticConfigParams validates the NMState data in a HostStaticNetworkConfig.
   219  func (s *staticNetworkConfigGenerator) ValidateStaticConfigParams(ctx context.Context, staticNetworkConfig []*models.HostStaticNetworkConfig) error {
   220  	var err *multierror.Error
   221  	for i, hostConfig := range staticNetworkConfig {
   222  		err = multierror.Append(err, s.validateMacInterfaceName(i, hostConfig.MacInterfaceMap))
   223  		if validateErr := s.ValidateNMStateYaml(ctx, hostConfig.NetworkYaml); validateErr != nil {
   224  			err = multierror.Append(err, fmt.Errorf("failed to validate network yaml for host %d, %w", i, validateErr))
   225  		}
   226  	}
   227  	return err.ErrorOrNil()
   228  }
   229  
   230  func (s *staticNetworkConfigGenerator) validateMacInterfaceName(hostIdx int, macInterfaceMap models.MacInterfaceMap) error {
   231  	interfaceCheck := make(map[string]struct{}, len(macInterfaceMap))
   232  	macCheck := make(map[string]struct{}, len(macInterfaceMap))
   233  	for _, macInterface := range macInterfaceMap {
   234  		interfaceCheck[macInterface.LogicalNicName] = struct{}{}
   235  		macCheck[macInterface.MacAddress] = struct{}{}
   236  	}
   237  	if len(interfaceCheck) < len(macInterfaceMap) || len(macCheck) < len(macInterfaceMap) {
   238  		return fmt.Errorf("MACs and Interfaces for host %d must be unique", hostIdx)
   239  	}
   240  	return nil
   241  }
   242  
   243  func (s *staticNetworkConfigGenerator) ValidateNMStateYaml(ctx context.Context, networkYaml string) error {
   244  	result, err := s.executeNMStatectl(ctx, networkYaml)
   245  	if err != nil {
   246  		return err
   247  	}
   248  
   249  	// Check that the file content can be created
   250  	// This doesn't write anything to the local filesystem
   251  	_, err = s.createNMConnectionFiles(result, "temphostdir")
   252  	return err
   253  }
   254  
   255  func compareMapInterfaces(intf1, intf2 *models.MacInterfaceMapItems0) bool {
   256  	if intf1.LogicalNicName != intf2.LogicalNicName {
   257  		return intf1.LogicalNicName < intf2.LogicalNicName
   258  	}
   259  	return intf1.MacAddress < intf2.MacAddress
   260  }
   261  
   262  func compareMacInterfaceMaps(map1, map2 models.MacInterfaceMap) bool {
   263  	if len(map1) != len(map2) {
   264  		return len(map1) < len(map2)
   265  	}
   266  	for i := range map1 {
   267  		less := compareMapInterfaces(map1[i], map2[i])
   268  		greater := compareMapInterfaces(map2[i], map1[i])
   269  		if less || greater {
   270  			return less
   271  		}
   272  	}
   273  	return false
   274  }
   275  
   276  func sortStaticNetworkConfig(staticNetworkConfig []*models.HostStaticNetworkConfig) {
   277  	for i := range staticNetworkConfig {
   278  		item := staticNetworkConfig[i]
   279  		sort.SliceStable(item.MacInterfaceMap, func(i, j int) bool {
   280  			return compareMapInterfaces(item.MacInterfaceMap[i], item.MacInterfaceMap[j])
   281  		})
   282  	}
   283  	sort.SliceStable(staticNetworkConfig, func(i, j int) bool {
   284  		hostConfig1 := staticNetworkConfig[i]
   285  		hostConfig2 := staticNetworkConfig[j]
   286  		if hostConfig1.NetworkYaml != hostConfig2.NetworkYaml {
   287  			return hostConfig1.NetworkYaml < hostConfig2.NetworkYaml
   288  		}
   289  		return compareMacInterfaceMaps(hostConfig1.MacInterfaceMap, hostConfig2.MacInterfaceMap)
   290  	})
   291  }
   292  
   293  // FormatStaticNetworkConfigForDB returns a sorted JSON representation of the network config.
   294  func (s *staticNetworkConfigGenerator) FormatStaticNetworkConfigForDB(staticNetworkConfig []*models.HostStaticNetworkConfig) (string, error) {
   295  	if len(staticNetworkConfig) == 0 {
   296  		return "", nil
   297  	}
   298  	sortStaticNetworkConfig(staticNetworkConfig)
   299  	b, err := json.Marshal(&staticNetworkConfig)
   300  	if err != nil {
   301  		return "", errors.Wrap(err, "Failed to JSON Marshal static network config")
   302  	}
   303  	return string(b), nil
   304  }
   305  
   306  func (s *staticNetworkConfigGenerator) decodeStaticNetworkConfig(staticNetworkConfigStr string) (staticNetworkConfig []*models.HostStaticNetworkConfig, err error) {
   307  	if staticNetworkConfigStr == "" {
   308  		return
   309  	}
   310  	err = json.Unmarshal([]byte(staticNetworkConfigStr), &staticNetworkConfig)
   311  	if err != nil {
   312  		return nil, errors.Wrapf(err, "Failed to JSON Unmarshal static network config %s", staticNetworkConfigStr)
   313  	}
   314  	return
   315  }
   316  
   317  func (s *staticNetworkConfigGenerator) formatMacInterfaceMap(macInterfaceMap models.MacInterfaceMap) string {
   318  	lines := make([]string, len(macInterfaceMap))
   319  	for i, entry := range macInterfaceMap {
   320  		lines[i] = fmt.Sprintf("%s=%s", entry.MacAddress, entry.LogicalNicName)
   321  	}
   322  	sort.Strings(lines)
   323  	return strings.Join(lines, "\n")
   324  }