github.com/makyo/juju@v0.0.0-20160425123129-2608902037e9/apiserver/common/networkingcommon/types.go (about) 1 // Copyright 2015 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 4 package networkingcommon 5 6 import ( 7 "encoding/json" 8 "net" 9 "regexp" 10 "sort" 11 "strings" 12 13 "github.com/juju/errors" 14 "github.com/juju/names" 15 "github.com/juju/utils/set" 16 17 "github.com/juju/juju/apiserver/params" 18 "github.com/juju/juju/cloudconfig/instancecfg" 19 "github.com/juju/juju/environs" 20 "github.com/juju/juju/environs/config" 21 "github.com/juju/juju/network" 22 providercommon "github.com/juju/juju/provider/common" 23 "github.com/juju/juju/state" 24 ) 25 26 // BackingSubnet defines the methods supported by a Subnet entity 27 // stored persistently. 28 // 29 // TODO(dimitern): Once the state backing is implemented, remove this 30 // and just use *state.Subnet. 31 type BackingSubnet interface { 32 CIDR() string 33 VLANTag() int 34 ProviderId() network.Id 35 AvailabilityZones() []string 36 Status() string 37 SpaceName() string 38 Life() params.Life 39 } 40 41 // BackingSubnetInfo describes a single subnet to be added in the 42 // backing store. 43 // 44 // TODO(dimitern): Replace state.SubnetInfo with this and remove 45 // BackingSubnetInfo, once the rest of state backing methods and the 46 // following pre-reqs are done: 47 // * subnetDoc.AvailabilityZone becomes subnetDoc.AvailabilityZones, 48 // adding an upgrade step to migrate existing non empty zones on 49 // subnet docs. Also change state.Subnet.AvailabilityZone to 50 // * add subnetDoc.SpaceName - no upgrade step needed, as it will only 51 // be used for new space-aware subnets. 52 // * Subnets need a reference count to calculate Status. 53 // * ensure EC2 and MAAS providers accept empty IDs as Subnets() args 54 // and return all subnets, including the AvailabilityZones (for EC2; 55 // empty for MAAS as zones are orthogonal to networks). 56 type BackingSubnetInfo struct { 57 // ProviderId is a provider-specific network id. This may be empty. 58 ProviderId network.Id 59 60 // CIDR of the network, in 123.45.67.89/24 format. 61 CIDR string 62 63 // VLANTag needs to be between 1 and 4094 for VLANs and 0 for normal 64 // networks. It's defined by IEEE 802.1Q standard. 65 VLANTag int 66 67 // AllocatableIPHigh and Low describe the allocatable portion of the 68 // subnet. The remainder, if any, is reserved by the provider. 69 // Either both of these must be set or neither, if they're empty it 70 // means that none of the subnet is allocatable. If present they must 71 // be valid IP addresses within the subnet CIDR. 72 AllocatableIPHigh string 73 AllocatableIPLow string 74 75 // AvailabilityZones describes which availability zone(s) this 76 // subnet is in. It can be empty if the provider does not support 77 // availability zones. 78 AvailabilityZones []string 79 80 // SpaceName holds the juju network space this subnet is 81 // associated with. Can be empty if not supported. 82 SpaceName string 83 84 // Status holds the status of the subnet. Normally this will be 85 // calculated from the reference count and Life of a subnet. 86 Status string 87 88 // Live holds the life of the subnet 89 Life params.Life 90 } 91 92 // BackingSpace defines the methods supported by a Space entity stored 93 // persistently. 94 type BackingSpace interface { 95 // Name returns the space name. 96 Name() string 97 98 // Subnets returns the subnets in the space 99 Subnets() ([]BackingSubnet, error) 100 101 // ProviderId returns the network ID of the provider 102 ProviderId() network.Id 103 104 // Zones returns a list of availability zone(s) that this 105 // space is in. It can be empty if the provider does not support 106 // availability zones. 107 Zones() []string 108 109 // Life returns the lifecycle state of the space 110 Life() params.Life 111 } 112 113 // Backing defines the methods needed by the API facade to store and 114 // retrieve information from the underlying persistency layer (state 115 // DB). 116 type NetworkBacking interface { 117 // ModelConfig returns the current environment config. 118 ModelConfig() (*config.Config, error) 119 120 // AvailabilityZones returns all cached availability zones (i.e. 121 // not from the provider, but in state). 122 AvailabilityZones() ([]providercommon.AvailabilityZone, error) 123 124 // SetAvailabilityZones replaces the cached list of availability 125 // zones with the given zones. 126 SetAvailabilityZones([]providercommon.AvailabilityZone) error 127 128 // AddSpace creates a space 129 AddSpace(Name string, ProviderId network.Id, Subnets []string, Public bool) error 130 131 // AllSpaces returns all known Juju network spaces. 132 AllSpaces() ([]BackingSpace, error) 133 134 // AddSubnet creates a backing subnet for an existing subnet. 135 AddSubnet(BackingSubnetInfo) (BackingSubnet, error) 136 137 // AllSubnets returns all backing subnets. 138 AllSubnets() ([]BackingSubnet, error) 139 } 140 141 func BackingSubnetToParamsSubnet(subnet BackingSubnet) params.Subnet { 142 cidr := subnet.CIDR() 143 vlantag := subnet.VLANTag() 144 providerid := subnet.ProviderId() 145 zones := subnet.AvailabilityZones() 146 status := subnet.Status() 147 var spaceTag names.SpaceTag 148 if subnet.SpaceName() != "" { 149 spaceTag = names.NewSpaceTag(subnet.SpaceName()) 150 } 151 152 return params.Subnet{ 153 CIDR: cidr, 154 VLANTag: vlantag, 155 ProviderId: string(providerid), 156 Zones: zones, 157 Status: status, 158 SpaceTag: spaceTag.String(), 159 Life: subnet.Life(), 160 } 161 } 162 163 type byMACThenCIDRThenIndexThenName []params.NetworkConfig 164 165 func (c byMACThenCIDRThenIndexThenName) Len() int { 166 return len(c) 167 } 168 169 func (c byMACThenCIDRThenIndexThenName) Swap(i, j int) { 170 orgI, orgJ := c[i], c[j] 171 c[j], c[i] = orgI, orgJ 172 } 173 174 func (c byMACThenCIDRThenIndexThenName) Less(i, j int) bool { 175 if c[i].MACAddress == c[j].MACAddress { 176 // Same MACAddress means related interfaces. 177 if c[i].CIDR == "" || c[j].CIDR == "" { 178 // Empty CIDRs go at the bottom, otherwise order by InterfaceName. 179 return c[i].CIDR != "" || c[i].InterfaceName < c[j].InterfaceName 180 } 181 if c[i].DeviceIndex == c[j].DeviceIndex { 182 if c[i].InterfaceName == c[j].InterfaceName { 183 // Sort addresses of the same interface. 184 return c[i].CIDR < c[j].CIDR || c[i].Address < c[j].Address 185 } 186 // Prefer shorter names (e.g. parents) with equal DeviceIndex. 187 return c[i].InterfaceName < c[j].InterfaceName 188 } 189 // When both CIDR and DeviceIndex are non-empty, order by DeviceIndex 190 return c[i].DeviceIndex < c[j].DeviceIndex 191 } 192 // Group by MACAddress. 193 return c[i].MACAddress < c[j].MACAddress 194 } 195 196 // SortNetworkConfigsByParents returns the given input sorted, such that any 197 // child interfaces appear after their parents. 198 func SortNetworkConfigsByParents(input []params.NetworkConfig) []params.NetworkConfig { 199 sortedInputCopy := CopyNetworkConfigs(input) 200 sort.Stable(byMACThenCIDRThenIndexThenName(sortedInputCopy)) 201 return sortedInputCopy 202 } 203 204 type byInterfaceName []params.NetworkConfig 205 206 func (c byInterfaceName) Len() int { 207 return len(c) 208 } 209 210 func (c byInterfaceName) Swap(i, j int) { 211 orgI, orgJ := c[i], c[j] 212 c[j], c[i] = orgI, orgJ 213 } 214 215 func (c byInterfaceName) Less(i, j int) bool { 216 return c[i].InterfaceName < c[j].InterfaceName 217 } 218 219 // SortNetworkConfigsByInterfaceName returns the given input sorted by 220 // InterfaceName. 221 func SortNetworkConfigsByInterfaceName(input []params.NetworkConfig) []params.NetworkConfig { 222 sortedInputCopy := CopyNetworkConfigs(input) 223 sort.Stable(byInterfaceName(sortedInputCopy)) 224 return sortedInputCopy 225 } 226 227 // NetworkConfigsToIndentedJSON returns the given input as an indented JSON 228 // string. 229 func NetworkConfigsToIndentedJSON(input []params.NetworkConfig) (string, error) { 230 jsonBytes, err := json.MarshalIndent(input, "", " ") 231 if err != nil { 232 return "", err 233 } 234 return string(jsonBytes), nil 235 } 236 237 // CopyNetworkConfigs returns a copy of the given input 238 func CopyNetworkConfigs(input []params.NetworkConfig) []params.NetworkConfig { 239 return append([]params.NetworkConfig(nil), input...) 240 } 241 242 // NetworkConfigFromInterfaceInfo converts a slice of network.InterfaceInfo into 243 // the equivalent params.NetworkConfig slice. 244 func NetworkConfigFromInterfaceInfo(interfaceInfos []network.InterfaceInfo) []params.NetworkConfig { 245 result := make([]params.NetworkConfig, len(interfaceInfos)) 246 for i, v := range interfaceInfos { 247 var dnsServers []string 248 for _, nameserver := range v.DNSServers { 249 dnsServers = append(dnsServers, nameserver.Value) 250 } 251 result[i] = params.NetworkConfig{ 252 DeviceIndex: v.DeviceIndex, 253 MACAddress: v.MACAddress, 254 CIDR: v.CIDR, 255 MTU: v.MTU, 256 ProviderId: string(v.ProviderId), 257 ProviderSubnetId: string(v.ProviderSubnetId), 258 ProviderSpaceId: string(v.ProviderSpaceId), 259 ProviderVLANId: string(v.ProviderVLANId), 260 ProviderAddressId: string(v.ProviderAddressId), 261 VLANTag: v.VLANTag, 262 InterfaceName: v.InterfaceName, 263 ParentInterfaceName: v.ParentInterfaceName, 264 InterfaceType: string(v.InterfaceType), 265 Disabled: v.Disabled, 266 NoAutoStart: v.NoAutoStart, 267 ConfigType: string(v.ConfigType), 268 Address: v.Address.Value, 269 DNSServers: dnsServers, 270 DNSSearchDomains: v.DNSSearchDomains, 271 GatewayAddress: v.GatewayAddress.Value, 272 } 273 } 274 return result 275 } 276 277 // NetworkConfigsToStateArgs splits the given networkConfig into a slice of 278 // state.LinkLayerDeviceArgs and a slice of state.LinkLayerDeviceAddress. The 279 // input is expected to come from MergeProviderAndObservedNetworkConfigs and to 280 // be sorted. 281 func NetworkConfigsToStateArgs(networkConfig []params.NetworkConfig) ( 282 []state.LinkLayerDeviceArgs, 283 []state.LinkLayerDeviceAddress, 284 ) { 285 var devicesArgs []state.LinkLayerDeviceArgs 286 var devicesAddrs []state.LinkLayerDeviceAddress 287 288 logger.Tracef("transforming network config to state args: %+v", networkConfig) 289 seenDeviceNames := set.NewStrings() 290 for _, netConfig := range networkConfig { 291 logger.Tracef("transforming device %q", netConfig.InterfaceName) 292 if !seenDeviceNames.Contains(netConfig.InterfaceName) { 293 // First time we see this, add it to devicesArgs. 294 seenDeviceNames.Add(netConfig.InterfaceName) 295 var mtu uint 296 if netConfig.MTU >= 0 { 297 mtu = uint(netConfig.MTU) 298 } 299 args := state.LinkLayerDeviceArgs{ 300 Name: netConfig.InterfaceName, 301 MTU: mtu, 302 ProviderID: network.Id(netConfig.ProviderId), 303 Type: state.LinkLayerDeviceType(netConfig.InterfaceType), 304 MACAddress: netConfig.MACAddress, 305 IsAutoStart: !netConfig.NoAutoStart, 306 IsUp: !netConfig.Disabled, 307 ParentName: netConfig.ParentInterfaceName, 308 } 309 logger.Tracef("state device args for device: %+v", args) 310 devicesArgs = append(devicesArgs, args) 311 } 312 313 if netConfig.CIDR == "" || netConfig.Address == "" { 314 logger.Tracef( 315 "skipping empty CIDR %q and/or Address %q of %q", 316 netConfig.CIDR, netConfig.Address, netConfig.InterfaceName, 317 ) 318 continue 319 } 320 _, ipNet, err := net.ParseCIDR(netConfig.CIDR) 321 if err != nil { 322 logger.Warningf("FIXME: ignoring unexpected CIDR format %q: %v", netConfig.CIDR, err) 323 continue 324 } 325 ipAddr := net.ParseIP(netConfig.Address) 326 if ipAddr == nil { 327 logger.Warningf("FIXME: ignoring unexpected Address format %q", netConfig.Address) 328 continue 329 } 330 ipNet.IP = ipAddr 331 cidrAddress := ipNet.String() 332 333 var derivedConfigMethod state.AddressConfigMethod 334 switch method := state.AddressConfigMethod(netConfig.ConfigType); method { 335 case state.StaticAddress, state.DynamicAddress, 336 state.LoopbackAddress, state.ManualAddress: 337 derivedConfigMethod = method 338 case "dhcp": // awkward special case 339 derivedConfigMethod = state.DynamicAddress 340 default: 341 derivedConfigMethod = state.StaticAddress 342 } 343 344 addr := state.LinkLayerDeviceAddress{ 345 DeviceName: netConfig.InterfaceName, 346 ProviderID: network.Id(netConfig.ProviderAddressId), 347 ConfigMethod: derivedConfigMethod, 348 CIDRAddress: cidrAddress, 349 DNSServers: netConfig.DNSServers, 350 DNSSearchDomains: netConfig.DNSSearchDomains, 351 GatewayAddress: netConfig.GatewayAddress, 352 } 353 logger.Tracef("state address args for device: %+v", addr) 354 devicesAddrs = append(devicesAddrs, addr) 355 } 356 logger.Tracef("seen devices: %+v", seenDeviceNames.SortedValues()) 357 logger.Tracef("network config transformed to state args:\n%+v\n%+v", devicesArgs, devicesAddrs) 358 return devicesArgs, devicesAddrs 359 } 360 361 // ModelConfigGetter is used to get the current model configuration. 362 type ModelConfigGetter interface { 363 ModelConfig() (*config.Config, error) 364 } 365 366 // NetworkingEnvironFromModelConfig constructs and returns 367 // environs.NetworkingEnviron using the given configGetter. Returns an error 368 // satisfying errors.IsNotSupported() if the model config does not support 369 // networking features. 370 func NetworkingEnvironFromModelConfig(configGetter ModelConfigGetter) (environs.NetworkingEnviron, error) { 371 modelConfig, err := configGetter.ModelConfig() 372 if err != nil { 373 return nil, errors.Annotate(err, "failed to get model config") 374 } 375 if modelConfig.Type() == "dummy" { 376 return nil, errors.NotSupportedf("dummy provider network config") 377 } 378 model, err := environs.New(modelConfig) 379 if err != nil { 380 return nil, errors.Annotate(err, "failed to construct a model from config") 381 } 382 netEnviron, supported := environs.SupportsNetworking(model) 383 if !supported { 384 // " not supported" will be appended to the message below. 385 return nil, errors.NotSupportedf("model %q networking", modelConfig.Name()) 386 } 387 return netEnviron, nil 388 } 389 390 var vlanInterfaceNameRegex = regexp.MustCompile(`^.+\.[0-9]{1,4}[^0-9]?$`) 391 392 var ( 393 netInterfaces = net.Interfaces 394 interfaceAddrs = (*net.Interface).Addrs 395 ) 396 397 // GetObservedNetworkConfig discovers what network interfaces exist on the 398 // machine, and returns that as a sorted slice of params.NetworkConfig to later 399 // update the state network config we have about the machine. 400 func GetObservedNetworkConfig() ([]params.NetworkConfig, error) { 401 logger.Tracef("discovering observed machine network config...") 402 403 interfaces, err := netInterfaces() 404 if err != nil { 405 return nil, errors.Annotate(err, "cannot get network interfaces") 406 } 407 408 var observedConfig []params.NetworkConfig 409 for _, nic := range interfaces { 410 isUp := nic.Flags&net.FlagUp > 0 411 412 derivedType := network.EthernetInterface 413 derivedConfigType := "" 414 if nic.Flags&net.FlagLoopback > 0 { 415 derivedType = network.LoopbackInterface 416 derivedConfigType = string(network.ConfigLoopback) 417 } else if vlanInterfaceNameRegex.MatchString(nic.Name) { 418 derivedType = network.VLAN_8021QInterface 419 } 420 421 nicConfig := params.NetworkConfig{ 422 DeviceIndex: nic.Index, 423 MACAddress: nic.HardwareAddr.String(), 424 ConfigType: derivedConfigType, 425 MTU: nic.MTU, 426 InterfaceName: nic.Name, 427 InterfaceType: string(derivedType), 428 NoAutoStart: !isUp, 429 Disabled: !isUp, 430 } 431 432 addrs, err := interfaceAddrs(&nic) 433 if err != nil { 434 return nil, errors.Annotatef(err, "cannot get interface %q addresses", nic.Name) 435 } 436 437 if len(addrs) == 0 { 438 observedConfig = append(observedConfig, nicConfig) 439 logger.Infof("no addresses observed on interface %q", nic.Name) 440 continue 441 } 442 443 for _, addr := range addrs { 444 cidrAddress := addr.String() 445 if cidrAddress == "" { 446 continue 447 } 448 ip, ipNet, err := net.ParseCIDR(cidrAddress) 449 if err != nil { 450 logger.Warningf("cannot parse interface %q address %q as CIDR: %v", nic.Name, cidrAddress, err) 451 if ip := net.ParseIP(cidrAddress); ip == nil { 452 return nil, errors.Errorf("cannot parse interface %q IP address %q", nic.Name, cidrAddress) 453 } else { 454 ipNet = &net.IPNet{} 455 } 456 ipNet.IP = ip 457 ipNet.Mask = net.IPv4Mask(255, 255, 255, 0) 458 logger.Infof("assuming interface %q has observed address %q", nic.Name, ipNet.String()) 459 } 460 if ip.To4() == nil { 461 logger.Debugf("skipping observed IPv6 address %q on %q: not fully supported yet", ip, nic.Name) 462 continue 463 } 464 465 nicConfigCopy := nicConfig 466 nicConfigCopy.CIDR = ipNet.String() 467 nicConfigCopy.Address = ip.String() 468 469 // TODO(dimitern): Add DNS servers, search domains, and gateway 470 // later. 471 472 observedConfig = append(observedConfig, nicConfigCopy) 473 } 474 } 475 sortedConfig := SortNetworkConfigsByParents(observedConfig) 476 477 logger.Tracef("about to update network config with observed: %+v", sortedConfig) 478 return sortedConfig, nil 479 } 480 481 // MergeProviderAndObservedNetworkConfigs returns the effective, sorted, network 482 // configs after merging providerConfig with observedConfig. 483 func MergeProviderAndObservedNetworkConfigs(providerConfigs, observedConfigs []params.NetworkConfig) []params.NetworkConfig { 484 providerConfigsByName := make(map[string][]params.NetworkConfig) 485 sortedProviderConfigs := SortNetworkConfigsByParents(providerConfigs) 486 for _, config := range sortedProviderConfigs { 487 name := config.InterfaceName 488 providerConfigsByName[name] = append(providerConfigsByName[name], config) 489 } 490 491 jsonProviderConfig, err := NetworkConfigsToIndentedJSON(sortedProviderConfigs) 492 if err != nil { 493 logger.Warningf("cannot serialize provider config %#v as JSON: %v", sortedProviderConfigs, err) 494 } else { 495 logger.Debugf("provider network config of machine:\n%s", jsonProviderConfig) 496 } 497 498 sortedObservedConfigs := SortNetworkConfigsByParents(observedConfigs) 499 500 jsonObservedConfig, err := NetworkConfigsToIndentedJSON(sortedObservedConfigs) 501 if err != nil { 502 logger.Warningf("cannot serialize observed config %#v as JSON: %v", sortedObservedConfigs, err) 503 } else { 504 logger.Debugf("observed network config of machine:\n%s", jsonObservedConfig) 505 } 506 507 var mergedConfigs []params.NetworkConfig 508 for _, config := range sortedObservedConfigs { 509 name := config.InterfaceName 510 logger.Tracef("merging observed config for device %q: %+v", name, config) 511 if strings.HasPrefix(name, instancecfg.DefaultBridgePrefix) { 512 logger.Tracef("found potential juju bridge %q in observed config", name) 513 unprefixedName := strings.TrimPrefix(name, instancecfg.DefaultBridgePrefix) 514 underlyingConfigs, underlyingKnownByProvider := providerConfigsByName[unprefixedName] 515 logger.Tracef("device %q underlying %q has provider config: %+v", name, unprefixedName, underlyingConfigs) 516 if underlyingKnownByProvider { 517 // This config is for a bridge created by Juju and not known by 518 // the provider. The bridge is configured to adopt the address 519 // allocated to the underlying interface, which is known by the 520 // provider. However, since the same underlying interface can 521 // have multiple addresses, we need to match the adopted 522 // bridgeConfig to the correct address. 523 524 var underlyingConfig params.NetworkConfig 525 for i, underlying := range underlyingConfigs { 526 if underlying.Address == config.Address { 527 logger.Tracef("replacing undelying config %+v", underlying) 528 // Remove what we found before changing it below. 529 underlyingConfig = underlying 530 underlyingConfigs = append(underlyingConfigs[:i], underlyingConfigs[i+1:]...) 531 break 532 } 533 } 534 logger.Tracef("underlying provider config after update: %+v", underlyingConfigs) 535 536 bridgeConfig := config 537 bridgeConfig.InterfaceType = string(network.BridgeInterface) 538 bridgeConfig.ConfigType = underlyingConfig.ConfigType 539 bridgeConfig.VLANTag = underlyingConfig.VLANTag 540 bridgeConfig.ProviderId = "" // Juju-created bridges never have a ProviderID 541 bridgeConfig.ProviderSpaceId = underlyingConfig.ProviderSpaceId 542 bridgeConfig.ProviderVLANId = underlyingConfig.ProviderVLANId 543 bridgeConfig.ProviderSubnetId = underlyingConfig.ProviderSubnetId 544 bridgeConfig.ProviderAddressId = underlyingConfig.ProviderAddressId 545 if underlyingParent := underlyingConfig.ParentInterfaceName; underlyingParent != "" { 546 bridgeConfig.ParentInterfaceName = instancecfg.DefaultBridgePrefix + underlyingParent 547 } 548 549 underlyingConfig.ConfigType = string(network.ConfigManual) 550 underlyingConfig.ParentInterfaceName = name 551 underlyingConfig.ProviderAddressId = "" 552 underlyingConfig.CIDR = "" 553 underlyingConfig.Address = "" 554 555 underlyingConfigs = append(underlyingConfigs, underlyingConfig) 556 providerConfigsByName[unprefixedName] = underlyingConfigs 557 logger.Tracef("updated provider network config by name: %+v", providerConfigsByName) 558 559 mergedConfigs = append(mergedConfigs, bridgeConfig) 560 continue 561 } 562 } 563 564 knownProviderConfigs, knownByProvider := providerConfigsByName[name] 565 if !knownByProvider { 566 // Not known by the provider and not a Juju-created bridge, so just 567 // use the observed config for it. 568 logger.Tracef("device %q not known to provider - adding only observed config: %+v", name, config) 569 mergedConfigs = append(mergedConfigs, config) 570 continue 571 } 572 logger.Tracef("device %q has known provider network config: %+v", name, knownProviderConfigs) 573 574 for _, providerConfig := range knownProviderConfigs { 575 if providerConfig.Address == config.Address { 576 logger.Tracef( 577 "device %q has observed address %q, index %d, and MTU %q; overriding index %d and MTU %d from provider config", 578 name, config.Address, config.DeviceIndex, config.MTU, providerConfig.DeviceIndex, providerConfig.MTU, 579 ) 580 // Prefer observed device indices and MTU values as more up-to-date. 581 providerConfig.DeviceIndex = config.DeviceIndex 582 providerConfig.MTU = config.MTU 583 584 mergedConfigs = append(mergedConfigs, providerConfig) 585 break 586 } 587 } 588 } 589 590 sortedMergedConfigs := SortNetworkConfigsByParents(mergedConfigs) 591 592 jsonMergedConfig, err := NetworkConfigsToIndentedJSON(sortedMergedConfigs) 593 if err != nil { 594 logger.Warningf("cannot serialize merged config %#v as JSON: %v", sortedMergedConfigs, err) 595 } else { 596 logger.Debugf("combined machine network config:\n%s", jsonMergedConfig) 597 } 598 599 return mergedConfigs 600 }