github.com/niedbalski/juju@v0.0.0-20190215020005-8ff100488e47/network/containerizer/bridgepolicy.go (about) 1 // Copyright 2017 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 4 package containerizer 5 6 import ( 7 "fmt" 8 "hash/crc32" 9 "sort" 10 "strings" 11 12 "github.com/juju/collections/set" 13 "github.com/juju/errors" 14 "github.com/juju/loggo" 15 16 "github.com/juju/juju/core/instance" 17 "github.com/juju/juju/network" 18 19 // Used for some constants and things like LinkLayerDevice[Args] 20 "github.com/juju/juju/state" 21 ) 22 23 var logger = loggo.GetLogger("juju.network.containerizer") 24 25 // BridgePolicy defines functionality that helps us create and define bridges 26 // for guests inside of a host machine, along with the creation of network 27 // devices on those bridges for the containers to use. 28 // Ideally BridgePolicy would be defined outside of the 'state' package as it 29 // doesn't deal directly with DB content, but not quite enough of State is exposed 30 type BridgePolicy struct { 31 // NetBondReconfigureDelay is how much of a delay to inject if we see that 32 // one of the devices being bridged is a BondDevice. This exists because of 33 // https://bugs.launchpad.net/juju/+bug/1657579 34 NetBondReconfigureDelay int 35 // ContainerNetworkingMethod defines the way containers are networked. 36 // It's one of: 37 // - fan 38 // - provider 39 // - local 40 ContainerNetworkingMethod string 41 } 42 43 // inferContainerSpaces tries to find a valid space for the container to be 44 // on. This should only be used when the container itself doesn't have any 45 // valid constraints on what spaces it should be in. 46 // If ContainerNetworkingMethod is 'local' we fall back to "" and use lxdbr0. 47 // If this machine is in a single space, then that space is used. Else, if 48 // the machine has the default space, then that space is used. 49 // If neither of those conditions is true, then we return an error. 50 func (p *BridgePolicy) inferContainerSpaces(m Machine, containerId, defaultSpaceName string) (set.Strings, error) { 51 if p.ContainerNetworkingMethod == "local" { 52 return set.NewStrings(""), nil 53 } 54 hostSpaces, err := m.AllSpaces() 55 if err != nil { 56 return nil, errors.Trace(err) 57 } 58 logger.Debugf("container %q not qualified to a space, host machine %q is using spaces %s", 59 containerId, m.Id(), network.QuoteSpaceSet(hostSpaces)) 60 if len(hostSpaces) == 1 { 61 return hostSpaces, nil 62 } 63 if defaultSpaceName != "" && hostSpaces.Contains(defaultSpaceName) { 64 return set.NewStrings(defaultSpaceName), nil 65 } 66 if len(hostSpaces) == 0 { 67 logger.Debugf("container has no desired spaces, " + 68 "and host has no known spaces, triggering fallback " + 69 "to bridge all devices") 70 return set.NewStrings(""), nil 71 } 72 return nil, errors.Errorf("no obvious space for container %q, host machine has spaces: %s", 73 containerId, network.QuoteSpaceSet(hostSpaces)) 74 } 75 76 // determineContainerSpaces tries to use the direct information about a 77 // container to find what spaces it should be in, and then falls back to what 78 // we know about the host machine. 79 func (p *BridgePolicy) determineContainerSpaces(m Machine, containerMachine Container, defaultSpaceName string) (set.Strings, error) { 80 containerSpaces, err := containerMachine.DesiredSpaces() 81 if err != nil { 82 return nil, errors.Trace(err) 83 } 84 logger.Debugf("for container %q, found desired spaces: %s", 85 containerMachine.Id(), network.QuoteSpaceSet(containerSpaces)) 86 if len(containerSpaces) == 0 { 87 // We have determined that the container doesn't have any useful 88 // constraints set on it. So lets see if we can come up with 89 // something useful. 90 containerSpaces, err = p.inferContainerSpaces(m, containerMachine.Id(), defaultSpaceName) 91 if err != nil { 92 return nil, errors.Trace(err) 93 } 94 } 95 return containerSpaces, nil 96 } 97 98 // findSpacesAndDevicesForContainer looks up what spaces the container wants 99 // to be in, and what spaces the host machine is already in, and tries to 100 // find the devices on the host that are useful for the container. 101 func (p *BridgePolicy) findSpacesAndDevicesForContainer(m Machine, containerMachine Container) (set.Strings, map[string][]LinkLayerDevice, error) { 102 containerSpaces, err := p.determineContainerSpaces(m, containerMachine, "") 103 if err != nil { 104 return nil, nil, errors.Trace(err) 105 } 106 devicesPerSpace, err := m.LinkLayerDevicesForSpaces(containerSpaces.Values()) 107 if err != nil { 108 logger.Errorf("findSpacesAndDevicesForContainer(%q) got error looking for host spaces: %v", 109 containerMachine.Id(), err) 110 return nil, nil, errors.Trace(err) 111 } 112 return containerSpaces, devicesPerSpace, nil 113 } 114 115 func possibleBridgeTarget(dev LinkLayerDevice) (bool, error) { 116 // LoopbackDevices can never be bridged 117 if dev.Type() == state.LoopbackDevice || dev.Type() == state.BridgeDevice { 118 return false, nil 119 } 120 // Devices that have no parent entry are direct host devices that can be 121 // bridged. 122 if dev.ParentName() == "" { 123 return true, nil 124 } 125 // TODO(jam): 2016-12-22 This feels dirty, but it falls out of how we are 126 // currently modeling VLAN objects. see bug https://pad.lv/1652049 127 if dev.Type() != state.VLAN_8021QDevice { 128 // Only state.VLAN_8021QDevice have parents that still allow us to bridge 129 // them. When anything else has a parent set, it shouldn't be used 130 return false, nil 131 } 132 parentDevice, err := dev.ParentDevice() 133 if err != nil { 134 // If we got an error here, we have some sort of 135 // database inconsistency error. 136 return false, err 137 } 138 if parentDevice.Type() == state.EthernetDevice || parentDevice.Type() == state.BondDevice { 139 // A plain VLAN device with a direct parent of its underlying 140 // ethernet device 141 return true, nil 142 } 143 return false, nil 144 } 145 146 func formatDeviceMap(spacesToDevices map[string][]LinkLayerDevice) string { 147 spaceNames := make([]string, len(spacesToDevices)) 148 i := 0 149 for spaceName := range spacesToDevices { 150 spaceNames[i] = spaceName 151 i++ 152 } 153 sort.Strings(spaceNames) 154 out := []string{} 155 for _, name := range spaceNames { 156 start := fmt.Sprintf("%q:[", name) 157 devices := spacesToDevices[name] 158 deviceNames := make([]string, len(devices)) 159 for i, dev := range devices { 160 deviceNames[i] = dev.Name() 161 } 162 deviceNames = network.NaturallySortDeviceNames(deviceNames...) 163 quotedNames := make([]string, len(deviceNames)) 164 for i, name := range deviceNames { 165 quotedNames[i] = fmt.Sprintf("%q", name) 166 } 167 out = append(out, start+strings.Join(quotedNames, ",")+"]") 168 } 169 return "map{" + strings.Join(out, ", ") + "}" 170 } 171 172 var skippedDeviceNames = set.NewStrings( 173 network.DefaultLXCBridge, 174 network.DefaultLXDBridge, 175 network.DefaultKVMBridge, 176 ) 177 178 // The general policy is to: 179 // 1. Add br- to device name (to keep current behaviour), if it doesn fit in 15 characters then: 180 // 2. Add b- to device name, if it doesn't fit in 15 characters then: 181 // 3a. For devices starting in 'en' remove 'en' and add 'b-' 182 // 3b. For all other devices 'b-' + 6-char hash of name + '-' + last 6 chars of name 183 // 4. If using the device name directly always replace '.' with '-', to make sure that bridges from VLANs won't break 184 func BridgeNameForDevice(device string) string { 185 device = strings.Replace(device, ".", "-", -1) 186 switch { 187 case len(device) < 13: 188 return fmt.Sprintf("br-%s", device) 189 case len(device) == 13: 190 return fmt.Sprintf("b-%s", device) 191 case device[:2] == "en": 192 return fmt.Sprintf("b-%s", device[2:]) 193 default: 194 hash := crc32.Checksum([]byte(device), crc32.IEEETable) & 0xffffff 195 return fmt.Sprintf("b-%0.6x-%s", hash, device[len(device)-6:]) 196 } 197 } 198 199 // FindMissingBridgesForContainer looks at the spaces that the container 200 // wants to be in, and sees if there are any host devices that should be 201 // bridged. 202 // This will return an Error if the container wants a space that the host 203 // machine cannot provide. 204 func (b *BridgePolicy) FindMissingBridgesForContainer(m Machine, containerMachine Container) ([]network.DeviceToBridge, int, error) { 205 reconfigureDelay := 0 206 containerSpaces, devicesPerSpace, err := b.findSpacesAndDevicesForContainer(m, containerMachine) 207 hostDeviceByName := make(map[string]LinkLayerDevice, 0) 208 if err != nil { 209 return nil, 0, errors.Trace(err) 210 } 211 logger.Debugf("FindMissingBridgesForContainer(%q) spaces %s devices %v", 212 containerMachine.Id(), network.QuoteSpaceSet(containerSpaces), 213 formatDeviceMap(devicesPerSpace)) 214 spacesFound := set.NewStrings() 215 fanSpacesFound := set.NewStrings() 216 for spaceName, devices := range devicesPerSpace { 217 for _, device := range devices { 218 if device.Type() == state.BridgeDevice { 219 if b.ContainerNetworkingMethod != "local" && skippedDeviceNames.Contains(device.Name()) { 220 continue 221 } 222 if strings.HasPrefix(device.Name(), "fan-") { 223 fanSpacesFound.Add(spaceName) 224 } else { 225 spacesFound.Add(spaceName) 226 } 227 } 228 } 229 } 230 notFound := containerSpaces.Difference(spacesFound) 231 fanNotFound := containerSpaces.Difference(fanSpacesFound) 232 if b.ContainerNetworkingMethod == "fan" { 233 if fanNotFound.IsEmpty() { 234 // Nothing to do, just return success 235 return nil, 0, nil 236 } else { 237 return nil, 0, errors.Errorf("host machine %q has no available FAN devices in space(s) %s", 238 m.Id(), network.QuoteSpaceSet(fanNotFound)) 239 } 240 } else { 241 if notFound.IsEmpty() { 242 // Nothing to do, just return success 243 return nil, 0, nil 244 } 245 } 246 hostDeviceNamesToBridge := make([]string, 0) 247 for _, spaceName := range notFound.Values() { 248 hostDeviceNames := make([]string, 0) 249 for _, hostDevice := range devicesPerSpace[spaceName] { 250 possible, err := possibleBridgeTarget(hostDevice) 251 if err != nil { 252 return nil, 0, err 253 } 254 if !possible { 255 continue 256 } 257 hostDeviceNames = append(hostDeviceNames, hostDevice.Name()) 258 hostDeviceByName[hostDevice.Name()] = hostDevice 259 spacesFound.Add(spaceName) 260 } 261 if len(hostDeviceNames) > 0 { 262 if spaceName == "" { 263 // When we are bridging unknown space devices, we bridge all 264 // of them. Both because this is a fallback, and because we 265 // don't know what the exact spaces are going to be. 266 for _, deviceName := range hostDeviceNames { 267 hostDeviceNamesToBridge = append(hostDeviceNamesToBridge, deviceName) 268 if hostDeviceByName[deviceName].Type() == state.BondDevice { 269 if reconfigureDelay < b.NetBondReconfigureDelay { 270 reconfigureDelay = b.NetBondReconfigureDelay 271 } 272 } 273 } 274 } else { 275 // This should already be sorted from 276 // LinkLayerDevicesForSpaces but sorting to be sure we stably 277 // pick the host device 278 hostDeviceNames = network.NaturallySortDeviceNames(hostDeviceNames...) 279 hostDeviceNamesToBridge = append(hostDeviceNamesToBridge, hostDeviceNames[0]) 280 if hostDeviceByName[hostDeviceNames[0]].Type() == state.BondDevice { 281 if reconfigureDelay < b.NetBondReconfigureDelay { 282 reconfigureDelay = b.NetBondReconfigureDelay 283 } 284 } 285 } 286 } 287 } 288 notFound = notFound.Difference(spacesFound) 289 if !notFound.IsEmpty() { 290 hostSpaces, err := m.AllSpaces() 291 if err != nil { 292 // log it, but we're returning another error right now 293 logger.Warningf("got error looking for spaces for host machine %q: %v", 294 m.Id(), err) 295 } 296 logger.Warningf("container %q wants spaces %s, but host machine %q has %s missing %s", 297 containerMachine.Id(), network.QuoteSpaceSet(containerSpaces), 298 m.Id(), network.QuoteSpaceSet(hostSpaces), network.QuoteSpaceSet(notFound)) 299 return nil, 0, errors.Errorf("host machine %q has no available device in space(s) %s", 300 m.Id(), network.QuoteSpaceSet(notFound)) 301 } 302 303 hostToBridge := make([]network.DeviceToBridge, 0, len(hostDeviceNamesToBridge)) 304 for _, hostName := range network.NaturallySortDeviceNames(hostDeviceNamesToBridge...) { 305 hostToBridge = append(hostToBridge, network.DeviceToBridge{ 306 DeviceName: hostName, 307 BridgeName: BridgeNameForDevice(hostName), 308 MACAddress: hostDeviceByName[hostName].MACAddress(), 309 }) 310 } 311 return hostToBridge, reconfigureDelay, nil 312 } 313 314 // PopulateContainerLinkLayerDevices sets the link-layer devices of the given 315 // containerMachine, setting each device linked to the corresponding 316 // BridgeDevice of the host machine. It also records when one of the 317 // desired spaces is available on the host machine, but not currently 318 // bridged. 319 func (p *BridgePolicy) PopulateContainerLinkLayerDevices(m Machine, containerMachine Container) error { 320 // TODO(jam): 20017-01-31 This doesn't quite feel right that we would be 321 // defining devices that 'will' exist in the container, but don't exist 322 // yet. If anything, this feels more like "Provider" level devices, because 323 // it is defining the devices from the outside, not the inside. 324 containerSpaces, devicesPerSpace, err := p.findSpacesAndDevicesForContainer(m, containerMachine) 325 if err != nil { 326 return errors.Trace(err) 327 } 328 logger.Debugf("for container %q, found host devices spaces: %s", 329 containerMachine.Id(), formatDeviceMap(devicesPerSpace)) 330 331 localBridgeForType := map[instance.ContainerType]string{ 332 instance.LXD: network.DefaultLXDBridge, 333 instance.KVM: network.DefaultKVMBridge, 334 } 335 spacesFound := set.NewStrings() 336 devicesByName := make(map[string]LinkLayerDevice) 337 bridgeDeviceNames := make([]string, 0) 338 339 for spaceName, hostDevices := range devicesPerSpace { 340 for _, hostDevice := range hostDevices { 341 isFan := strings.HasPrefix(hostDevice.Name(), "fan-") 342 wantThisDevice := isFan == (p.ContainerNetworkingMethod == "fan") 343 deviceType, name := hostDevice.Type(), hostDevice.Name() 344 if wantThisDevice && deviceType == state.BridgeDevice && !skippedDeviceNames.Contains(name) { 345 devicesByName[name] = hostDevice 346 bridgeDeviceNames = append(bridgeDeviceNames, name) 347 spacesFound.Add(spaceName) 348 } 349 } 350 } 351 missingSpace := containerSpaces.Difference(spacesFound) 352 353 // Check if we are missing "" and can fill it in with a local bridge 354 if len(missingSpace) == 1 && missingSpace.Contains("") && p.ContainerNetworkingMethod == "local" { 355 localBridgeName := localBridgeForType[containerMachine.ContainerType()] 356 for _, hostDevice := range devicesPerSpace[""] { 357 name := hostDevice.Name() 358 if hostDevice.Type() == state.BridgeDevice && name == localBridgeName { 359 missingSpace.Remove("") 360 devicesByName[name] = hostDevice 361 bridgeDeviceNames = append(bridgeDeviceNames, name) 362 spacesFound.Add("") 363 } 364 } 365 } 366 if len(missingSpace) > 0 { 367 logger.Warningf("container %q wants spaces %s could not find host %q bridges for %s, found bridges %s", 368 containerMachine.Id(), network.QuoteSpaceSet(containerSpaces), 369 m.Id(), network.QuoteSpaceSet(missingSpace), bridgeDeviceNames) 370 return errors.Errorf("unable to find host bridge for space(s) %s for container %q", 371 network.QuoteSpaceSet(missingSpace), containerMachine.Id()) 372 } 373 374 sortedBridgeDeviceNames := network.NaturallySortDeviceNames(bridgeDeviceNames...) 375 logger.Debugf("for container %q using host machine %q bridge devices: %s", 376 containerMachine.Id(), m.Id(), network.QuoteSpaces(sortedBridgeDeviceNames)) 377 containerDevicesArgs := make([]state.LinkLayerDeviceArgs, len(bridgeDeviceNames)) 378 379 for i, hostBridgeName := range sortedBridgeDeviceNames { 380 hostBridge := devicesByName[hostBridgeName] 381 newLLD, err := hostBridge.EthernetDeviceForBridge(fmt.Sprintf("eth%d", i)) 382 if err != nil { 383 return errors.Trace(err) 384 } 385 containerDevicesArgs[i] = newLLD 386 } 387 logger.Debugf("prepared container %q network config: %+v", containerMachine.Id(), containerDevicesArgs) 388 389 if err := containerMachine.SetLinkLayerDevices(containerDevicesArgs...); err != nil { 390 return errors.Trace(err) 391 } 392 393 logger.Debugf("container %q network config set", containerMachine.Id()) 394 return nil 395 }