github.com/juju/juju@v0.0.0-20240430160146-1752b71fcf00/state/subnets.go (about)

     1  // Copyright 2014 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package state
     5  
     6  import (
     7  	"strconv"
     8  
     9  	"github.com/juju/collections/set"
    10  	"github.com/juju/errors"
    11  	"github.com/juju/mgo/v3"
    12  	"github.com/juju/mgo/v3/bson"
    13  	"github.com/juju/mgo/v3/txn"
    14  	jujutxn "github.com/juju/txn/v3"
    15  
    16  	"github.com/juju/juju/core/network"
    17  	"github.com/juju/juju/mongo"
    18  )
    19  
    20  type Subnet struct {
    21  	st  *State
    22  	doc subnetDoc
    23  	// spaceID is either the space ID from the subnet's FanLocalUnderlay,
    24  	// or this subnet's space ID.
    25  	spaceID string
    26  }
    27  
    28  type subnetDoc struct {
    29  	DocID             string   `bson:"_id"`
    30  	TxnRevno          int64    `bson:"txn-revno"`
    31  	ID                string   `bson:"subnet-id"`
    32  	ModelUUID         string   `bson:"model-uuid"`
    33  	Life              Life     `bson:"life"`
    34  	ProviderId        string   `bson:"providerid,omitempty"`
    35  	ProviderNetworkId string   `bson:"provider-network-id,omitempty"`
    36  	CIDR              string   `bson:"cidr"`
    37  	VLANTag           int      `bson:"vlantag,omitempty"`
    38  	AvailabilityZones []string `bson:"availability-zones,omitempty"`
    39  	IsPublic          bool     `bson:"is-public,omitempty"`
    40  	SpaceID           string   `bson:"space-id,omitempty"`
    41  	FanLocalUnderlay  string   `bson:"fan-local-underlay,omitempty"`
    42  	FanOverlay        string   `bson:"fan-overlay,omitempty"`
    43  }
    44  
    45  // Life returns whether the subnet is Alive, Dying or Dead.
    46  func (s *Subnet) Life() Life {
    47  	return s.doc.Life
    48  }
    49  
    50  // ID returns the unique ID for the subnet.
    51  func (s *Subnet) ID() string {
    52  	return s.doc.ID
    53  }
    54  
    55  // String implements fmt.Stringer.
    56  func (s *Subnet) String() string {
    57  	return s.CIDR()
    58  }
    59  
    60  // GoString implements fmt.GoStringer.
    61  func (s *Subnet) GoString() string {
    62  	return s.String()
    63  }
    64  
    65  func (s *Subnet) FanOverlay() string {
    66  	return s.doc.FanOverlay
    67  }
    68  
    69  func (s *Subnet) FanLocalUnderlay() string {
    70  	return s.doc.FanLocalUnderlay
    71  }
    72  
    73  // IsPublic returns true if the subnet is public.
    74  func (s *Subnet) IsPublic() bool {
    75  	return s.doc.IsPublic
    76  }
    77  
    78  // EnsureDead sets the Life of the subnet to Dead if it is Alive.
    79  // If the subnet is already Dead, no error is returned.
    80  // When the subnet is no longer Alive or already removed,
    81  // errNotAlive is returned.
    82  func (s *Subnet) EnsureDead() (err error) {
    83  	defer errors.DeferredAnnotatef(&err, "cannot set subnet %q to dead", s)
    84  
    85  	if s.doc.Life == Dead {
    86  		return nil
    87  	}
    88  
    89  	ops := []txn.Op{{
    90  		C:      subnetsC,
    91  		Id:     s.doc.DocID,
    92  		Update: bson.D{{"$set", bson.D{{"life", Dead}}}},
    93  		Assert: isAliveDoc,
    94  	}}
    95  
    96  	txnErr := s.st.db().RunTransaction(ops)
    97  	if txnErr == nil {
    98  		s.doc.Life = Dead
    99  		return nil
   100  	}
   101  	return onAbort(txnErr, subnetNotAliveErr)
   102  }
   103  
   104  // Remove removes a Dead subnet. If the subnet is not Dead or it is already
   105  // removed, an error is returned. On success, all IP addresses added to the
   106  // subnet are also removed.
   107  func (s *Subnet) Remove() (err error) {
   108  	defer errors.DeferredAnnotatef(&err, "cannot remove subnet %q", s)
   109  
   110  	if s.doc.Life != Dead {
   111  		return errors.New("subnet is not dead")
   112  	}
   113  
   114  	ops := []txn.Op{{
   115  		C:      subnetsC,
   116  		Id:     s.doc.DocID,
   117  		Remove: true,
   118  		Assert: isDeadDoc,
   119  	}}
   120  	if s.doc.ProviderId != "" {
   121  		op := s.st.networkEntityGlobalKeyRemoveOp("subnet", s.ProviderId())
   122  		ops = append(ops, op)
   123  	}
   124  
   125  	txnErr := s.st.db().RunTransaction(ops)
   126  	if txnErr == nil {
   127  		return nil
   128  	}
   129  	return onAbort(txnErr, errors.New("not found or not dead"))
   130  }
   131  
   132  // ProviderId returns the provider-specific ID of the subnet.
   133  func (s *Subnet) ProviderId() network.Id {
   134  	return network.Id(s.doc.ProviderId)
   135  }
   136  
   137  // CIDR returns the subnet CIDR (e.g. 192.168.50.0/24).
   138  func (s *Subnet) CIDR() string {
   139  	return s.doc.CIDR
   140  }
   141  
   142  // VLANTag returns the subnet VLAN tag. It's a number between 1 and
   143  // 4094 for VLANs and 0 if the network is not a VLAN.
   144  func (s *Subnet) VLANTag() int {
   145  	return s.doc.VLANTag
   146  }
   147  
   148  // AvailabilityZones returns the availability zones of the subnet.
   149  // If the subnet is not associated with an availability zones
   150  // it will return an the empty slice.
   151  func (s *Subnet) AvailabilityZones() []string {
   152  	return s.doc.AvailabilityZones
   153  }
   154  
   155  // SpaceName returns the space the subnet is associated with.  If no
   156  // space is associated, return the default space and log an error.
   157  func (s *Subnet) SpaceName() string {
   158  	if s.spaceID == "" {
   159  		logger.Errorf("subnet %q has no spaceID", s.spaceID)
   160  		return network.AlphaSpaceName
   161  	}
   162  	sp, err := s.st.Space(s.spaceID)
   163  	if err != nil {
   164  		logger.Errorf("error finding space %q: %s", s.spaceID, err)
   165  		return network.AlphaSpaceName
   166  	}
   167  	return sp.Name()
   168  }
   169  
   170  // SpaceID returns the ID of the space the subnet is associated with.
   171  // If the subnet is not associated with a space it will return
   172  // network.AlphaSpaceId.
   173  func (s *Subnet) SpaceID() string {
   174  	return s.spaceID
   175  }
   176  
   177  // UpdateSubnetSpaceOps returns operations that will ensure that
   178  // the subnet is in the input space, provided the space exists.
   179  func (st *State) UpdateSubnetSpaceOps(subnetID, spaceID string) []txn.Op {
   180  	if subnet, err := st.Subnet(subnetID); err == nil && subnet.SpaceID() == spaceID {
   181  		return nil
   182  	}
   183  	return []txn.Op{
   184  		{
   185  			C:      spacesC,
   186  			Id:     st.docID(spaceID),
   187  			Assert: txn.DocExists,
   188  		},
   189  		{
   190  			C:      subnetsC,
   191  			Id:     st.docID(subnetID),
   192  			Update: bson.D{{"$set", bson.D{{"space-id", spaceID}}}},
   193  			Assert: isAliveDoc,
   194  		},
   195  	}
   196  }
   197  
   198  // ProviderNetworkId returns the provider id of the network containing
   199  // this subnet.
   200  func (s *Subnet) ProviderNetworkId() network.Id {
   201  	return network.Id(s.doc.ProviderNetworkId)
   202  }
   203  
   204  // Refresh refreshes the contents of the Subnet from the underlying
   205  // state. It an error that satisfies errors.IsNotFound if the SubnetByCIDR has
   206  // been removed.
   207  func (s *Subnet) Refresh() error {
   208  	subnets, closer := s.st.db().GetCollection(subnetsC)
   209  	defer closer()
   210  
   211  	err := subnets.FindId(s.doc.DocID).One(&s.doc)
   212  	if err == mgo.ErrNotFound {
   213  		return errors.NotFoundf("subnet %q", s)
   214  	}
   215  	if err != nil {
   216  		return errors.Errorf("cannot refresh subnet %q: %v", s, err)
   217  	}
   218  	if err = s.setSpace(subnets); err != nil {
   219  		return err
   220  	}
   221  	return nil
   222  }
   223  
   224  func (s *Subnet) setSpace(subnets mongo.Collection) error {
   225  	s.spaceID = s.doc.SpaceID
   226  	if s.doc.FanLocalUnderlay == "" {
   227  		return nil
   228  	}
   229  	if subnets == nil {
   230  		// Some callers have the mongo subnet collection already; some do not.
   231  		var closer SessionCloser
   232  		subnets, closer = s.st.db().GetCollection(subnetsC)
   233  		defer closer()
   234  	}
   235  	overlayDoc := &subnetDoc{}
   236  	// TODO: (hml) 2019-08-06
   237  	// Rethink the bson logic once multiple subnets can have the same cidr.
   238  	err := subnets.Find(bson.M{"cidr": s.doc.FanLocalUnderlay}).One(overlayDoc)
   239  	if err == mgo.ErrNotFound {
   240  		logger.Errorf("unable to update spaceID for subnet %q %q: underlay network %q: %s",
   241  			s.doc.ID, s.doc.CIDR, s.doc.FanLocalUnderlay, err.Error())
   242  		return nil
   243  	}
   244  	if err != nil {
   245  		return errors.Annotatef(err, "underlay network %v for FAN %v", s.doc.FanLocalUnderlay, s.doc.CIDR)
   246  	}
   247  	s.spaceID = overlayDoc.SpaceID
   248  	return nil
   249  }
   250  
   251  // Update adds new info to the subnet based on input info.
   252  // Currently no data is changed unless it is the "undefined" space from MAAS.
   253  // There are restrictions on the additions allowed:
   254  //   - No change to CIDR; more work is required to determine how to handle.
   255  //   - No change to ProviderId nor ProviderNetworkID; these are immutable.
   256  func (s *Subnet) Update(args network.SubnetInfo) error {
   257  	buildTxn := func(attempt int) ([]txn.Op, error) {
   258  		if attempt != 0 {
   259  			if err := s.Refresh(); err != nil {
   260  				if errors.IsNotFound(err) {
   261  					return nil, errors.Errorf("ProviderId %q not unique", args.ProviderId)
   262  				}
   263  				return nil, errors.Trace(err)
   264  			}
   265  		}
   266  		makeSpaceNameUpdate, err := s.updateSpaceName(args.SpaceName)
   267  		if err != nil {
   268  			return nil, err
   269  		}
   270  		var bsonSet bson.D
   271  		if makeSpaceNameUpdate {
   272  			// TODO (hml) 2019-07-25
   273  			// Update for SpaceID once SubnetInfo Updated
   274  			sp, err := s.st.SpaceByName(args.SpaceName)
   275  			if err != nil {
   276  				return nil, errors.Trace(err)
   277  			}
   278  			bsonSet = append(bsonSet, bson.DocElem{Name: "space-id", Value: sp.Id()})
   279  		}
   280  		if len(args.AvailabilityZones) > 0 {
   281  			currentAZ := set.NewStrings(args.AvailabilityZones...)
   282  			newAZ := currentAZ.Difference(set.NewStrings(s.doc.AvailabilityZones...))
   283  			if !newAZ.IsEmpty() {
   284  				bsonSet = append(bsonSet,
   285  					bson.DocElem{Name: "availability-zones", Value: append(s.doc.AvailabilityZones, newAZ.Values()...)})
   286  			}
   287  		}
   288  		if s.doc.VLANTag == 0 && args.VLANTag > 0 {
   289  			bsonSet = append(bsonSet, bson.DocElem{Name: "vlantag", Value: args.VLANTag})
   290  		}
   291  		if len(bsonSet) == 0 {
   292  			return nil, jujutxn.ErrNoOperations
   293  		}
   294  		return []txn.Op{{
   295  			C:      subnetsC,
   296  			Id:     s.doc.DocID,
   297  			Assert: bson.D{{"txn-revno", s.doc.TxnRevno}},
   298  			Update: bson.D{{"$set", bsonSet}},
   299  		}}, nil
   300  	}
   301  	return errors.Trace(s.st.db().Run(buildTxn))
   302  }
   303  
   304  func (s *Subnet) updateSpaceName(spaceName string) (bool, error) {
   305  	var spaceNameChange bool
   306  	sp, err := s.st.Space(s.doc.SpaceID)
   307  	switch {
   308  	case err != nil && !errors.IsNotFound(err):
   309  		return false, errors.Trace(err)
   310  	case errors.IsNotFound(err):
   311  		spaceNameChange = true
   312  	case err == nil:
   313  		// Only change space name it's a default one at this time.
   314  		//
   315  		// The undefined space from MAAS has a providerId of -1.
   316  		// The juju default space will be 0.
   317  		spaceNameChange = sp.doc.ProviderId == "-1" || sp.doc.Id == network.AlphaSpaceId
   318  	}
   319  	// TODO (hml) 2019-07-25
   320  	// Update when there is a s.doc.spaceID, which has done the calculation of
   321  	// ID from the CIDR or FAN.
   322  	return spaceNameChange && spaceName != "" && s.doc.FanLocalUnderlay == "", nil
   323  }
   324  
   325  // AllSubnetInfos returns SubnetInfos for all subnets in the model.
   326  func (st *State) AllSubnetInfos() (network.SubnetInfos, error) {
   327  	subs, err := st.AllSubnets()
   328  	if err != nil {
   329  		return nil, errors.Trace(err)
   330  	}
   331  
   332  	result := make(network.SubnetInfos, len(subs))
   333  	for i, sub := range subs {
   334  		result[i] = sub.networkSubnet()
   335  	}
   336  	return result, nil
   337  }
   338  
   339  // networkSubnet maps the subnet fields into a network.SubnetInfo.
   340  // Note that this method should not be exported.
   341  // It is only called on subnets that are guaranteed, if Fan overlays,
   342  // to have had their space IDs correctly set based on their underlays.
   343  // Calling it on an overlay not processed in this way will yield a
   344  // space ID of "0", which may be incorrect.
   345  func (s *Subnet) networkSubnet() network.SubnetInfo {
   346  	var fanInfo *network.FanCIDRs
   347  	if s.doc.FanLocalUnderlay != "" || s.doc.FanOverlay != "" {
   348  		fanInfo = &network.FanCIDRs{
   349  			FanLocalUnderlay: s.doc.FanLocalUnderlay,
   350  			FanOverlay:       s.doc.FanOverlay,
   351  		}
   352  	}
   353  
   354  	sInfo := network.SubnetInfo{
   355  		ID:                network.Id(s.doc.ID),
   356  		CIDR:              s.doc.CIDR,
   357  		ProviderId:        network.Id(s.doc.ProviderId),
   358  		ProviderNetworkId: network.Id(s.doc.ProviderNetworkId),
   359  		VLANTag:           s.doc.VLANTag,
   360  		AvailabilityZones: s.doc.AvailabilityZones,
   361  		FanInfo:           fanInfo,
   362  		IsPublic:          s.doc.IsPublic,
   363  		SpaceID:           s.spaceID,
   364  		Life:              s.Life().Value(),
   365  		// SpaceName and ProviderSpaceID are populated by Space.NetworkSpace().
   366  		// For now, we do not look them up here.
   367  	}
   368  
   369  	return sInfo
   370  }
   371  
   372  // SubnetUpdate adds new info to the subnet based on provided info.
   373  func (st *State) SubnetUpdate(args network.SubnetInfo) error {
   374  	s, err := st.SubnetByCIDR(args.CIDR)
   375  	if err != nil {
   376  		return errors.Trace(err)
   377  	}
   378  	return s.Update(args)
   379  }
   380  
   381  // AddSubnet creates and returns a new subnet.
   382  func (st *State) AddSubnet(args network.SubnetInfo) (subnet *Subnet, err error) {
   383  	defer errors.DeferredAnnotatef(&err, "adding subnet %q", args.CIDR)
   384  
   385  	if err := args.Validate(); err != nil {
   386  		return nil, errors.Trace(err)
   387  	}
   388  
   389  	var seq int
   390  	if seq, err = sequence(st, "subnet"); err != nil {
   391  		return nil, errors.Trace(err)
   392  	}
   393  
   394  	buildTxn := func(attempt int) ([]txn.Op, error) {
   395  		if attempt != 0 {
   396  			if err := checkModelActive(st); err != nil {
   397  				return nil, errors.Trace(err)
   398  			}
   399  			if _, err = st.Subnet(subnet.ID()); err == nil {
   400  				return nil, errors.AlreadyExistsf("subnet %q", args.CIDR)
   401  			}
   402  			if err := subnet.Refresh(); err != nil {
   403  				if errors.IsNotFound(err) {
   404  					return nil, errors.Errorf("provider ID %q not unique", args.ProviderId)
   405  				}
   406  				return nil, errors.Trace(err)
   407  			}
   408  		}
   409  		var ops []txn.Op
   410  		var subDoc subnetDoc
   411  		subDoc, ops, err = st.addSubnetOps(strconv.Itoa(seq), args)
   412  		if err != nil {
   413  			return nil, errors.Trace(err)
   414  		}
   415  		subnet = &Subnet{st: st, doc: subDoc}
   416  		ops = append(ops, assertModelActiveOp(st.ModelUUID()))
   417  		return ops, nil
   418  	}
   419  	err = st.db().Run(buildTxn)
   420  	if err != nil {
   421  		return nil, errors.Trace(err)
   422  	}
   423  	if err := subnet.setSpace(nil); err != nil {
   424  		return nil, errors.Trace(err)
   425  	}
   426  	return subnet, nil
   427  }
   428  
   429  // AddSubnetOps returns transaction operations required to ensure that the
   430  // input subnet is added to state.
   431  func (st *State) AddSubnetOps(args network.SubnetInfo) ([]txn.Op, error) {
   432  	seq, err := sequence(st, "subnet")
   433  	if err != nil {
   434  		return nil, errors.Trace(err)
   435  	}
   436  
   437  	_, ops, err := st.addSubnetOps(strconv.Itoa(seq), args)
   438  	return ops, errors.Trace(err)
   439  }
   440  
   441  func (st *State) addSubnetOps(id string, args network.SubnetInfo) (subnetDoc, []txn.Op, error) {
   442  	unique, err := st.uniqueSubnet(args.CIDR, string(args.ProviderId))
   443  	if err != nil {
   444  		return subnetDoc{}, nil, errors.Trace(err)
   445  	}
   446  	if !unique {
   447  		return subnetDoc{}, nil, errors.AlreadyExistsf("subnet %q", args.CIDR)
   448  	}
   449  
   450  	// Unless explicitly placed, new subnets go into the alpha space.
   451  	// TODO (manadart 2020-08-12): We should determine the model's configured
   452  	// default space and put it there instead.
   453  	if args.SpaceID == "" {
   454  		args.SpaceID = network.AlphaSpaceId
   455  	}
   456  
   457  	subDoc := subnetDoc{
   458  		DocID:             st.docID(id),
   459  		ID:                id,
   460  		ModelUUID:         st.ModelUUID(),
   461  		Life:              Alive,
   462  		CIDR:              args.CIDR,
   463  		VLANTag:           args.VLANTag,
   464  		ProviderId:        string(args.ProviderId),
   465  		ProviderNetworkId: string(args.ProviderNetworkId),
   466  		AvailabilityZones: args.AvailabilityZones,
   467  		SpaceID:           args.SpaceID,
   468  		FanLocalUnderlay:  args.FanLocalUnderlay(),
   469  		FanOverlay:        args.FanOverlay(),
   470  		IsPublic:          args.IsPublic,
   471  	}
   472  	ops := []txn.Op{
   473  		{
   474  			C:      subnetsC,
   475  			Id:     subDoc.DocID,
   476  			Assert: txn.DocMissing,
   477  			Insert: subDoc,
   478  		},
   479  	}
   480  	if args.ProviderId != "" {
   481  		ops = append(ops, st.networkEntityGlobalKeyOp("subnet", args.ProviderId))
   482  	}
   483  	return subDoc, ops, nil
   484  }
   485  
   486  func (st *State) uniqueSubnet(cidr, providerID string) (bool, error) {
   487  	subnets, closer := st.db().GetCollection(subnetsC)
   488  	defer closer()
   489  
   490  	pID := bson.D{{"providerid", providerID}}
   491  	if providerID == "" {
   492  		pID = bson.D{{"providerid", bson.D{{"$exists", false}}}}
   493  	}
   494  
   495  	count, err := subnets.Find(
   496  		bson.D{{"$and",
   497  			[]bson.D{
   498  				{{"cidr", cidr}},
   499  				pID,
   500  			},
   501  		}}).Count()
   502  
   503  	if err == mgo.ErrNotFound {
   504  		return false, errors.NotFoundf("subnet CIDR %q", cidr)
   505  	}
   506  	if err != nil {
   507  		return false, errors.Annotatef(err, "querying subnet CIDR %q", cidr)
   508  	}
   509  	return count == 0, nil
   510  }
   511  
   512  // Subnet returns the subnet identified by the input ID,
   513  // or an error if it is not found.
   514  func (st *State) Subnet(id string) (*Subnet, error) {
   515  	subnets, err := st.subnets(bson.M{"subnet-id": id})
   516  	if err != nil {
   517  		return nil, errors.Annotatef(err, "retrieving subnet with ID %q", id)
   518  	}
   519  	if len(subnets) == 0 {
   520  		return nil, errors.NotFoundf("subnet %q", id)
   521  	}
   522  	return subnets[0], nil
   523  }
   524  
   525  // SubnetByCIDR returns a unique subnet matching the input CIDR.
   526  // If no unique match is achieved, an error is returned.
   527  // TODO (manadart 2020-03-11): As of this date, CIDR remains a unique
   528  // identifier for a subnet due to how we constrain provider networking
   529  // implementations. When this changes, callers relying on this method to return
   530  // a unique match will need attention.
   531  // Usage of this method should probably be phased out.
   532  func (st *State) SubnetByCIDR(cidr string) (*Subnet, error) {
   533  	subnets, err := st.subnets(bson.M{"cidr": cidr})
   534  	if err != nil {
   535  		return nil, errors.Annotatef(err, "retrieving subnet with CIDR %q", cidr)
   536  	}
   537  	if len(subnets) == 0 {
   538  		return nil, errors.NotFoundf("subnet %q", cidr)
   539  	}
   540  	if len(subnets) > 1 {
   541  		return nil, errors.Errorf("multiple subnets matching %q", cidr)
   542  	}
   543  	return subnets[0], nil
   544  }
   545  
   546  // SubnetsByCIDR returns the subnets matching the input CIDR.
   547  func (st *State) SubnetsByCIDR(cidr string) ([]*Subnet, error) {
   548  	subnets, err := st.subnets(bson.M{"cidr": cidr})
   549  	return subnets, errors.Annotatef(err, "retrieving subnets with CIDR %q", cidr)
   550  }
   551  
   552  func (st *State) subnets(exp bson.M) ([]*Subnet, error) {
   553  	col, closer := st.db().GetCollection(subnetsC)
   554  	defer closer()
   555  
   556  	var docs []subnetDoc
   557  	err := col.Find(exp).All(&docs)
   558  	if err != nil {
   559  		return nil, errors.Trace(err)
   560  	}
   561  	if len(docs) == 0 {
   562  		return nil, nil
   563  	}
   564  
   565  	subnets := make([]*Subnet, len(docs))
   566  	for i, doc := range docs {
   567  		subnets[i] = &Subnet{st: st, doc: doc}
   568  		if err := subnets[i].setSpace(col); err != nil {
   569  			return nil, errors.Trace(err)
   570  		}
   571  	}
   572  	return subnets, nil
   573  }
   574  
   575  // AllSubnets returns all known subnets in the model.
   576  func (st *State) AllSubnets() (subnets []*Subnet, err error) {
   577  	subnetsCollection, closer := st.db().GetCollection(subnetsC)
   578  	defer closer()
   579  
   580  	var docs []subnetDoc
   581  	err = subnetsCollection.Find(nil).All(&docs)
   582  	if err != nil {
   583  		return nil, errors.Annotatef(err, "cannot get all subnets")
   584  	}
   585  	cidrToSpace := make(map[string]string)
   586  	for _, doc := range docs {
   587  		cidrToSpace[doc.CIDR] = doc.SpaceID
   588  	}
   589  	for _, doc := range docs {
   590  		spaceID := doc.SpaceID
   591  		if doc.FanLocalUnderlay != "" {
   592  			if space, ok := cidrToSpace[doc.FanLocalUnderlay]; ok {
   593  				spaceID = space
   594  			}
   595  		}
   596  		subnets = append(subnets, &Subnet{st: st, doc: doc, spaceID: spaceID})
   597  	}
   598  	return subnets, nil
   599  }