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  }