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  }