github.com/juju/juju@v0.0.0-20240430160146-1752b71fcf00/core/network/space.go (about)

     1  // Copyright 2019 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package network
     5  
     6  import (
     7  	"fmt"
     8  	"net"
     9  	"regexp"
    10  	"strings"
    11  
    12  	"github.com/juju/collections/set"
    13  	"github.com/juju/errors"
    14  )
    15  
    16  const (
    17  	// AlphaSpaceId is the ID of the alpha network space.
    18  	// Application endpoints are bound to this space by default
    19  	// if no explicit binding is specified.
    20  	AlphaSpaceId = "0"
    21  
    22  	// AlphaSpaceName is the name of the alpha network space.
    23  	AlphaSpaceName = "alpha"
    24  )
    25  
    26  // SpaceLookup describes the ability to get a complete
    27  // network topology, as understood by Juju.
    28  type SpaceLookup interface {
    29  	AllSpaceInfos() (SpaceInfos, error)
    30  }
    31  
    32  // SubnetLookup describes retrieving all subnets within a known set of spaces.
    33  type SubnetLookup interface {
    34  	AllSubnetInfos() (SubnetInfos, error)
    35  }
    36  
    37  // SpaceName is the name of a network space.
    38  type SpaceName string
    39  
    40  // SpaceInfo defines a network space.
    41  type SpaceInfo struct {
    42  	// ID is the unique identifier for the space.
    43  	// TODO (manadart 2020-04-10): This should be a typed ID.
    44  	ID string
    45  
    46  	// Name is the name of the space.
    47  	// It is used by operators for identifying a space and should be unique.
    48  	Name SpaceName
    49  
    50  	// ProviderId is the provider's unique identifier for the space,
    51  	// such as used by MAAS.
    52  	ProviderId Id
    53  
    54  	// Subnets are the subnets that have been grouped into this network space.
    55  	Subnets SubnetInfos
    56  }
    57  
    58  // SpaceInfos is a collection of spaces.
    59  type SpaceInfos []SpaceInfo
    60  
    61  // AllSpaceInfos satisfies the SpaceLookup interface.
    62  // It is useful for passing to conversions where we already have the spaces
    63  // materialised and don't need to pull them from the DB again.
    64  func (s SpaceInfos) AllSpaceInfos() (SpaceInfos, error) {
    65  	return s, nil
    66  }
    67  
    68  // AllSubnetInfos returns all subnets contained in this collection of spaces.
    69  // Since a subnet can only be in one space, we can simply accrue them all
    70  // with the need for duplicate checking.
    71  // As with AllSpaceInfos, it implements an interface that can be used to
    72  // indirect state.
    73  func (s SpaceInfos) AllSubnetInfos() (SubnetInfos, error) {
    74  	subs := make(SubnetInfos, 0)
    75  	for _, space := range s {
    76  		for _, sub := range space.Subnets {
    77  			subs = append(subs, sub)
    78  		}
    79  	}
    80  	return subs, nil
    81  }
    82  
    83  // FanOverlaysFor returns any subnets in this network topology that are
    84  // fan overlays for the input subnet IDs.
    85  func (s SpaceInfos) FanOverlaysFor(subnetIDs IDSet) (SubnetInfos, error) {
    86  	if len(subnetIDs) == 0 {
    87  		return nil, nil
    88  	}
    89  
    90  	var located int
    91  	var allOverlays SubnetInfos
    92  
    93  	for _, space := range s {
    94  		for _, sub := range space.Subnets {
    95  			if subnetIDs.Contains(sub.ID) {
    96  				overlays, err := space.Subnets.GetByUnderlayCIDR(sub.CIDR)
    97  				if err != nil {
    98  					return nil, errors.Trace(err)
    99  				}
   100  				allOverlays = append(allOverlays, overlays...)
   101  
   102  				// If we have tested all of the inputs, we can exit early.
   103  				located++
   104  				if located >= len(subnetIDs) {
   105  					return allOverlays, nil
   106  				}
   107  			}
   108  		}
   109  	}
   110  
   111  	return allOverlays, nil
   112  }
   113  
   114  // MoveSubnets returns a new topology representing
   115  // the movement of subnets to a new network space.
   116  func (s SpaceInfos) MoveSubnets(subnetIDs IDSet, spaceName string) (SpaceInfos, error) {
   117  	newSpace := s.GetByName(spaceName)
   118  	if newSpace == nil {
   119  		return nil, errors.NotFoundf("space with name %q", spaceName)
   120  	}
   121  
   122  	// We return a copy, not mutating the original.
   123  	newSpaces := make(SpaceInfos, len(s))
   124  	var movers SubnetInfos
   125  	found := MakeIDSet()
   126  
   127  	// First accrue the moving subnets and remove them from their old spaces.
   128  	for i, space := range s {
   129  		newSpaces[i] = space
   130  		newSpaces[i].Subnets = nil
   131  
   132  		for _, sub := range space.Subnets {
   133  			if subnetIDs.Contains(sub.ID) {
   134  				// Indicate that we found the subnet,
   135  				// but don't do anything if it is already in the space.
   136  				found.Add(sub.ID)
   137  				if string(space.Name) != spaceName {
   138  					sub.SpaceID = newSpace.ID
   139  					sub.SpaceName = spaceName
   140  					sub.ProviderSpaceId = newSpace.ProviderId
   141  					movers = append(movers, sub)
   142  				}
   143  				continue
   144  			}
   145  			newSpaces[i].Subnets = append(newSpaces[i].Subnets, sub)
   146  		}
   147  	}
   148  
   149  	// Ensure that the input did not include subnets not in this collection.
   150  	if diff := subnetIDs.Difference(found); len(diff) != 0 {
   151  		return nil, errors.NotFoundf("subnet IDs %v", diff.SortedValues())
   152  	}
   153  
   154  	// Then put them against the new one.
   155  	// We have to find the space again in this collection,
   156  	// because newSpace was returned from a copy.
   157  	for i, space := range newSpaces {
   158  		if string(space.Name) == spaceName {
   159  			newSpaces[i].Subnets = append(space.Subnets, movers...)
   160  			break
   161  		}
   162  	}
   163  
   164  	return newSpaces, nil
   165  }
   166  
   167  // String returns returns a quoted, comma-delimited names of the spaces in the
   168  // collection, or <none> if the collection is empty.
   169  func (s SpaceInfos) String() string {
   170  	if len(s) == 0 {
   171  		return "<none>"
   172  	}
   173  	names := make([]string, len(s))
   174  	for i, v := range s {
   175  		names[i] = fmt.Sprintf("%q", string(v.Name))
   176  	}
   177  	return strings.Join(names, ", ")
   178  }
   179  
   180  // Names returns a string slice with each of the space names in the collection.
   181  func (s SpaceInfos) Names() []string {
   182  	names := make([]string, len(s))
   183  	for i, v := range s {
   184  		names[i] = string(v.Name)
   185  	}
   186  	return names
   187  }
   188  
   189  // IDs returns a string slice with each of the space ids in the collection.
   190  func (s SpaceInfos) IDs() []string {
   191  	ids := make([]string, len(s))
   192  	for i, v := range s {
   193  		ids[i] = v.ID
   194  	}
   195  	return ids
   196  }
   197  
   198  // GetByID returns a reference to the space with the input ID
   199  // if it exists in the collection. Otherwise nil is returned.
   200  func (s SpaceInfos) GetByID(id string) *SpaceInfo {
   201  	for _, space := range s {
   202  		if space.ID == id {
   203  			return &space
   204  		}
   205  	}
   206  	return nil
   207  }
   208  
   209  // GetByName returns a reference to the space with the input name
   210  // if it exists in the collection. Otherwise nil is returned.
   211  func (s SpaceInfos) GetByName(name string) *SpaceInfo {
   212  	for _, space := range s {
   213  		if string(space.Name) == name {
   214  			return &space
   215  		}
   216  	}
   217  	return nil
   218  }
   219  
   220  // ContainsID returns true if the collection contains a
   221  // space with the given ID.
   222  func (s SpaceInfos) ContainsID(id string) bool {
   223  	return s.GetByID(id) != nil
   224  }
   225  
   226  // ContainsName returns true if the collection contains a
   227  // space with the given name.
   228  func (s SpaceInfos) ContainsName(name string) bool {
   229  	return s.GetByName(name) != nil
   230  }
   231  
   232  // Minus returns a new SpaceInfos representing all the
   233  // values in the target that are not in the parameter.
   234  // Value matching is done by ID.
   235  func (s SpaceInfos) Minus(other SpaceInfos) SpaceInfos {
   236  	result := make(SpaceInfos, 0)
   237  	for _, value := range s {
   238  		if !other.ContainsID(value.ID) {
   239  			result = append(result, value)
   240  		}
   241  	}
   242  	return result
   243  }
   244  
   245  func (s SpaceInfos) InferSpaceFromAddress(addr string) (*SpaceInfo, error) {
   246  	var (
   247  		ip    = net.ParseIP(addr)
   248  		match *SpaceInfo
   249  	)
   250  
   251  nextSpace:
   252  	for spIndex, space := range s {
   253  		for _, subnet := range space.Subnets {
   254  			ipNet, err := subnet.ParsedCIDRNetwork()
   255  			if err != nil {
   256  				// Subnets should always have a valid CIDR
   257  				return nil, errors.Trace(err)
   258  			}
   259  
   260  			if ipNet.Contains(ip) {
   261  				if match == nil {
   262  					match = &s[spIndex]
   263  
   264  					// We still need to check other spaces
   265  					// in case we have multiple networks
   266  					// with the same subnet CIDRs
   267  					continue nextSpace
   268  				}
   269  
   270  				return nil, errors.Errorf(
   271  					"unable to infer space for address %q: address matches the same CIDR in multiple spaces", addr)
   272  			}
   273  		}
   274  	}
   275  
   276  	if match == nil {
   277  		return nil, errors.NewNotFound(nil, fmt.Sprintf("unable to infer space for address %q", addr))
   278  	}
   279  	return match, nil
   280  }
   281  
   282  func (s SpaceInfos) InferSpaceFromCIDRAndSubnetID(cidr, providerSubnetID string) (*SpaceInfo, error) {
   283  	for _, space := range s {
   284  		for _, subnet := range space.Subnets {
   285  			if subnet.CIDR == cidr && string(subnet.ProviderId) == providerSubnetID {
   286  				return &space, nil
   287  			}
   288  		}
   289  	}
   290  
   291  	return nil, errors.NewNotFound(
   292  		nil, fmt.Sprintf("unable to infer space for CIDR %q and provider subnet ID %q", cidr, providerSubnetID))
   293  }
   294  
   295  // SubnetCIDRsBySpaceID returns the set of known subnet CIDRs grouped by the
   296  // space ID they belong to.
   297  func (s SpaceInfos) SubnetCIDRsBySpaceID() map[string][]string {
   298  	res := make(map[string][]string)
   299  	for _, space := range s {
   300  		for _, sub := range space.Subnets {
   301  			res[space.ID] = append(res[space.ID], sub.CIDR)
   302  		}
   303  	}
   304  	return res
   305  }
   306  
   307  var (
   308  	invalidSpaceNameChars = regexp.MustCompile("[^0-9a-z-]")
   309  	dashPrefix            = regexp.MustCompile("^-*")
   310  	dashSuffix            = regexp.MustCompile("-*$")
   311  	multipleDashes        = regexp.MustCompile("--+")
   312  )
   313  
   314  // ConvertSpaceName is used to massage provider-sourced (i.e. MAAS)
   315  // space names so that they conform to Juju's space name rules.
   316  func ConvertSpaceName(name string, existing set.Strings) string {
   317  	// Lower case and replace spaces with dashes.
   318  	name = strings.Replace(name, " ", "-", -1)
   319  	name = strings.ToLower(name)
   320  
   321  	// Remove any character not in the set "-", "a-z", "0-9".
   322  	name = invalidSpaceNameChars.ReplaceAllString(name, "")
   323  
   324  	// Remove any dashes at the beginning and end.
   325  	name = dashPrefix.ReplaceAllString(name, "")
   326  	name = dashSuffix.ReplaceAllString(name, "")
   327  
   328  	// Replace multiple dashes with a single dash.
   329  	name = multipleDashes.ReplaceAllString(name, "-")
   330  
   331  	// If the name had only invalid characters, give it a new name.
   332  	if name == "" {
   333  		name = "empty"
   334  	}
   335  
   336  	// If this name is in use add a numerical suffix.
   337  	if existing.Contains(name) {
   338  		counter := 2
   339  		for existing.Contains(fmt.Sprintf("%s-%d", name, counter)) {
   340  			counter++
   341  		}
   342  		name = fmt.Sprintf("%s-%d", name, counter)
   343  	}
   344  
   345  	return name
   346  }