github.com/juju/juju@v0.0.0-20240327075706-a90865de2538/container/lxd/network.go (about) 1 // Copyright 2018 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 4 package lxd 5 6 import ( 7 "fmt" 8 "sort" 9 "strconv" 10 "strings" 11 12 "github.com/canonical/lxd/shared/api" 13 "github.com/juju/errors" 14 15 corenetwork "github.com/juju/juju/core/network" 16 "github.com/juju/juju/network" 17 ) 18 19 const ( 20 nic = "nic" 21 nicTypeBridged = "bridged" 22 nicTypeMACVLAN = "macvlan" 23 netTypeBridge = "bridge" 24 ) 25 26 // device is a type alias for profile devices. 27 type device = map[string]string 28 29 // LocalBridgeName returns the name of the local LXD network bridge. 30 func (s *Server) LocalBridgeName() string { 31 return s.localBridgeName 32 } 33 34 // EnableHTTPSListener configures LXD to listen for HTTPS requests, rather than 35 // only via a Unix socket. Attempts to listen on all protocols, but falls back 36 // to IPv4 only if IPv6 has been disabled with in kernel. 37 // Returns an error if updating the server configuration fails. 38 func (s *Server) EnableHTTPSListener() error { 39 // Make sure the LXD service is configured to listen to local https 40 // requests, rather than only via the Unix socket. 41 // TODO: jam 2016-02-25 This tells LXD to listen on all addresses, 42 // which does expose the LXD to outside requests. It would 43 // probably be better to only tell LXD to listen for requests on 44 // the loopback and LXC bridges that we are using. 45 if err := s.UpdateServerConfig(map[string]string{ 46 "core.https_address": "[::]", 47 }); err != nil { 48 cause := errors.Cause(err) 49 if strings.HasSuffix(cause.Error(), errIPV6NotSupported) { 50 // Fall back to IPv4 only. 51 return errors.Trace(s.UpdateServerConfig(map[string]string{ 52 "core.https_address": "0.0.0.0", 53 })) 54 } 55 return errors.Trace(err) 56 } 57 return nil 58 } 59 60 // EnsureIPv4 retrieves the network for the input name and checks its IPv4 61 // configuration. If none is detected, it is set to "auto". 62 // The boolean return indicates if modification was necessary. 63 func (s *Server) EnsureIPv4(netName string) (bool, error) { 64 var modified bool 65 66 net, eTag, err := s.GetNetwork(netName) 67 if err != nil { 68 return false, errors.Trace(err) 69 } 70 71 cfg, ok := net.Config["ipv4.address"] 72 if !ok || cfg == "none" { 73 if net.Config == nil { 74 net.Config = make(device, 2) 75 } 76 net.Config["ipv4.address"] = "auto" 77 net.Config["ipv4.nat"] = "true" 78 79 if err := s.UpdateNetwork(netName, net.Writable(), eTag); err != nil { 80 return false, errors.Trace(err) 81 } 82 modified = true 83 } 84 85 return modified, nil 86 } 87 88 // GetNICsFromProfile returns all NIC devices in the profile with the input 89 // name. All returned devices have a MAC address; generated if required. 90 func (s *Server) GetNICsFromProfile(profileName string) (map[string]device, error) { 91 profile, _, err := s.GetProfile(profileName) 92 if err != nil { 93 return nil, errors.Trace(err) 94 } 95 96 nics := getProfileNICs(profile) 97 for name := range nics { 98 if nics[name]["hwaddr"] == "" { 99 nics[name]["hwaddr"] = corenetwork.GenerateVirtualMACAddress() 100 } 101 } 102 return nics, nil 103 } 104 105 // VerifyNetworkDevice attempts to ensure that there is a network usable by LXD 106 // and that there is a NIC device with said network as its parent. 107 // If there are no NIC devices, and this server is *not* in cluster mode, 108 // an attempt is made to create an new device in the input profile, 109 // with the default LXD bridge as its parent. 110 func (s *Server) VerifyNetworkDevice(profile *api.Profile, eTag string) error { 111 nics := getProfileNICs(profile) 112 113 if len(nics) == 0 { 114 if s.networkAPISupport && !s.clustered { 115 return errors.Annotate(s.ensureDefaultNetworking(profile, eTag), "ensuring default bridge config") 116 } 117 return errors.Errorf("profile %q does not have any devices configured with type %q", profile.Name, nic) 118 } 119 120 if !s.networkAPISupport { 121 return errors.NotSupportedf("versions of LXD without network API") 122 } 123 124 return errors.Annotatef(s.verifyNICsWithAPI(nics), "profile %q", profile.Name) 125 } 126 127 // ensureDefaultNetworking ensures that the default LXD bridge exists, 128 // that it is not configured to use IPv6, and that a NIC device exists in 129 // the input profile. 130 // An error is returned if the bridge exists with IPv6 configuration. 131 // If the bridge does not exist, it is created. 132 func (s *Server) ensureDefaultNetworking(profile *api.Profile, eTag string) error { 133 net, _, err := s.GetNetwork(network.DefaultLXDBridge) 134 if err != nil { 135 if !IsLXDNotFound(err) { 136 return errors.Trace(err) 137 } 138 req := api.NetworksPost{ 139 Name: network.DefaultLXDBridge, 140 Type: netTypeBridge, 141 NetworkPut: api.NetworkPut{Config: map[string]string{ 142 "ipv4.address": "auto", 143 "ipv4.nat": "true", 144 "ipv6.address": "none", 145 "ipv6.nat": "false", 146 }}, 147 } 148 err := s.CreateNetwork(req) 149 if err != nil { 150 return errors.Trace(err) 151 } 152 net, _, err = s.GetNetwork(network.DefaultLXDBridge) 153 if err != nil { 154 return errors.Trace(err) 155 } 156 } 157 158 s.localBridgeName = network.DefaultLXDBridge 159 160 nicName := generateNICDeviceName(profile) 161 if nicName == "" { 162 return errors.Errorf("failed to generate a unique device name for profile %q", profile.Name) 163 } 164 165 // Add the new device with the bridge as its parent. 166 nicType := nicTypeMACVLAN 167 if net.Type == netTypeBridge { 168 nicType = nicTypeBridged 169 } 170 profile.Devices[nicName] = device{ 171 "type": nic, 172 "nictype": nicType, 173 "parent": network.DefaultLXDBridge, 174 } 175 176 if err := s.UpdateProfile(profile.Name, profile.Writable(), eTag); err != nil { 177 return errors.Trace(err) 178 } 179 logger.Debugf("created new nic device %q in profile %q", nicName, profile.Name) 180 return nil 181 } 182 183 // verifyNICsWithAPI uses the LXD network API to check if one of the input NIC 184 // devices is suitable for LXD to work with Juju. 185 func (s *Server) verifyNICsWithAPI(nics map[string]device) error { 186 checked := make([]string, 0, len(nics)) 187 for name, nic := range nics { 188 checked = append(checked, name) 189 190 // Versions of the LXD profile prior to 3.22 have the network name as 191 // "parent" under NIC entries in the "devices" list. 192 // Later versions have it under "network". 193 netName, ok := nic["network"] 194 if !ok { 195 netName = nic["parent"] 196 } 197 if netName == "" { 198 continue 199 } 200 201 net, _, err := s.GetNetwork(netName) 202 if err != nil { 203 return errors.Annotatef(err, "retrieving network %q", netName) 204 } 205 206 // Versions of the LXD profile prior to 3.22 have a "nictype" member 207 // under NIC entries in the "devices" list. 208 // Later versions were observed to have this member absent, 209 // however this information is available from the actual network. 210 if !isValidNetworkType(net) && !isValidNICType(nic) { 211 continue 212 } 213 214 logger.Tracef("found usable network device %q with parent %q", name, netName) 215 s.localBridgeName = netName 216 return nil 217 } 218 219 // No nics with a nictype of nicTypeBridged, nicTypeMACVLAN was found. 220 return errors.Errorf(fmt.Sprintf( 221 "no network device found with nictype %q or %q"+ 222 "\n\tthe following devices were checked: %s"+ 223 "\nReconfigure lxd to use a network of type %q or %q.", 224 nicTypeBridged, nicTypeMACVLAN, strings.Join(checked, ", "), nicTypeBridged, nicTypeMACVLAN)) 225 } 226 227 // generateNICDeviceName attempts to generate a new NIC device name that is not 228 // already in the input profile. If none can be determined in a reasonable 229 // search space, an empty name is returned. This should never really happen, 230 // but the name generation aborts to be safe from (theoretical) integer overflow. 231 func generateNICDeviceName(profile *api.Profile) string { 232 template := "eth%d" 233 for i := 0; i < 100; i++ { 234 name := fmt.Sprintf(template, i) 235 unique := true 236 for d := range profile.Devices { 237 if d == name { 238 unique = false 239 break 240 } 241 } 242 if unique { 243 return name 244 } 245 } 246 return "" 247 } 248 249 // getProfileNICs iterates over the devices in the input profile and returns 250 // any that are of type "nic". 251 func getProfileNICs(profile *api.Profile) map[string]device { 252 nics := make(map[string]device, len(profile.Devices)) 253 for k, v := range profile.Devices { 254 if v["type"] == nic { 255 nics[k] = v 256 } 257 } 258 return nics 259 } 260 261 func isValidNICType(nic device) bool { 262 return nic["nictype"] == nicTypeBridged || nic["nictype"] == nicTypeMACVLAN 263 } 264 265 func isValidNetworkType(net *api.Network) bool { 266 return net.Type == netTypeBridge || net.Type == nicTypeMACVLAN 267 } 268 269 // InterfaceInfoFromDevices returns a slice of interface info congruent with the 270 // input LXD NIC devices. 271 // The output is used to generate cloud-init user-data congruent with the NICs 272 // that end up in the container. 273 func InterfaceInfoFromDevices(nics map[string]device) (corenetwork.InterfaceInfos, error) { 274 interfaces := make(corenetwork.InterfaceInfos, len(nics)) 275 var i int 276 for name, device := range nics { 277 iInfo := corenetwork.InterfaceInfo{ 278 InterfaceName: name, 279 ParentInterfaceName: device["parent"], 280 MACAddress: device["hwaddr"], 281 ConfigType: corenetwork.ConfigDHCP, 282 Origin: corenetwork.OriginProvider, 283 } 284 if device["mtu"] != "" { 285 mtu, err := strconv.Atoi(device["mtu"]) 286 if err != nil { 287 return nil, errors.Annotate(err, "parsing device MTU") 288 } 289 iInfo.MTU = mtu 290 } 291 292 interfaces[i] = iInfo 293 i++ 294 } 295 296 sortInterfacesByName(interfaces) 297 return interfaces, nil 298 } 299 300 type interfaceInfoSlice corenetwork.InterfaceInfos 301 302 func (s interfaceInfoSlice) Len() int { return len(s) } 303 func (s interfaceInfoSlice) Swap(i, j int) { s[i], s[j] = s[j], s[i] } 304 func (s interfaceInfoSlice) Less(i, j int) bool { 305 return s[i].InterfaceName < s[j].InterfaceName 306 } 307 308 func sortInterfacesByName(interfaces corenetwork.InterfaceInfos) { 309 sort.Sort(interfaceInfoSlice(interfaces)) 310 } 311 312 // errIPV6NotSupported is the error returned by glibc for attempts at 313 // unsupported protocols. 314 const errIPV6NotSupported = `socket: address family not supported by protocol` 315 316 // DevicesFromInterfaceInfo uses the input interface info collection to create 317 // a map of network device configuration in the LXD format. Names for any 318 // networks without a known CIDR are returned in a slice. 319 func DevicesFromInterfaceInfo(interfaces corenetwork.InterfaceInfos) (map[string]device, []string, error) { 320 nics := make(map[string]device, len(interfaces)) 321 var unknown []string 322 for _, v := range interfaces { 323 if v.InterfaceType == corenetwork.LoopbackDevice { 324 continue 325 } 326 if v.InterfaceType != corenetwork.EthernetDevice { 327 return nil, nil, errors.Errorf("interface type %q not supported", v.InterfaceType) 328 } 329 if v.ParentInterfaceName == "" { 330 return nil, nil, errors.Errorf("parent interface name is empty") 331 } 332 if v.PrimaryAddress().CIDR == "" { 333 unknown = append(unknown, v.ParentInterfaceName) 334 } 335 nics[v.InterfaceName] = newNICDevice(v.InterfaceName, v.ParentInterfaceName, v.MACAddress, v.MTU) 336 } 337 338 return nics, unknown, nil 339 } 340 341 // newNICDevice creates and returns a LXD-compatible config for a network 342 // device from the input arguments. 343 // TODO (manadart 2018-06-21) We want to support nictype=macvlan too. 344 // This will involve interrogating the parent device, via the server if it is 345 // LXD managed, or via the container.NetworkConfig.DeviceType that this is 346 // being generated from. 347 func newNICDevice(deviceName, parentDevice, hwAddr string, mtu int) device { 348 device := map[string]string{ 349 "type": "nic", 350 "nictype": nicTypeBridged, 351 "name": deviceName, 352 "parent": parentDevice, 353 } 354 if hwAddr != "" { 355 device["hwaddr"] = hwAddr 356 } 357 if mtu > 0 { 358 device["mtu"] = fmt.Sprintf("%v", mtu) 359 } 360 return device 361 }