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 }