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 }