github.com/niedbalski/juju@v0.0.0-20190215020005-8ff100488e47/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  	"io/ioutil"
     9  	"os"
    10  	"sort"
    11  	"strconv"
    12  	"strings"
    13  
    14  	"github.com/juju/errors"
    15  	"github.com/lxc/lxd/shared/api"
    16  
    17  	"github.com/juju/juju/network"
    18  )
    19  
    20  const (
    21  	nic            = "nic"
    22  	nicTypeBridged = "bridged"
    23  	nicTypeMACVLAN = "macvlan"
    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(profName string) (map[string]device, error) {
    91  	profile, _, err := s.GetProfile(lxdDefaultProfileName)
    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"] = network.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.Annotatef(s.verifyNICsWithAPI(nics), "profile %q", profile.Name)
   122  	}
   123  
   124  	return errors.Annotatef(s.verifyNICsWithConfigFile(nics, ioutil.ReadFile), "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:    "bridge",
   141  			Managed: true,
   142  			NetworkPut: api.NetworkPut{Config: map[string]string{
   143  				"ipv4.address": "auto",
   144  				"ipv4.nat":     "true",
   145  				"ipv6.address": "none",
   146  				"ipv6.nat":     "false",
   147  			}},
   148  		}
   149  		err := s.CreateNetwork(req)
   150  		if err != nil {
   151  			return errors.Trace(err)
   152  		}
   153  		net, _, err = s.GetNetwork(network.DefaultLXDBridge)
   154  		if err != nil {
   155  			return errors.Trace(err)
   156  		}
   157  	} else {
   158  		if err := verifyNoIPv6(net); err != nil {
   159  			return errors.Trace(err)
   160  		}
   161  	}
   162  
   163  	s.localBridgeName = network.DefaultLXDBridge
   164  
   165  	nicName := generateNICDeviceName(profile)
   166  	if nicName == "" {
   167  		return errors.Errorf("failed to generate a unique device name for profile %q", profile.Name)
   168  	}
   169  
   170  	// Add the new device with the bridge as its parent.
   171  	nicType := nicTypeMACVLAN
   172  	if net.Type == "bridge" {
   173  		nicType = nicTypeBridged
   174  	}
   175  	profile.Devices[nicName] = device{
   176  		"type":    nic,
   177  		"nictype": nicType,
   178  		"parent":  network.DefaultLXDBridge,
   179  	}
   180  
   181  	if err := s.UpdateProfile(profile.Name, profile.Writable(), eTag); err != nil {
   182  		return errors.Trace(err)
   183  	}
   184  	logger.Debugf("created new nic device %q in profile %q", nicName, profile.Name)
   185  	return nil
   186  }
   187  
   188  // verifyNICsWithAPI uses the LXD network API to check if one of the input NIC
   189  // devices is suitable for LXD to work with Juju.
   190  func (s *Server) verifyNICsWithAPI(nics map[string]device) error {
   191  	checked := make([]string, 0, len(nics))
   192  	var ipV6ErrMsg error
   193  	for name, nic := range nics {
   194  		checked = append(checked, name)
   195  
   196  		if !isValidNICType(nic) {
   197  			continue
   198  		}
   199  
   200  		netName := nic["parent"]
   201  		if netName == "" {
   202  			continue
   203  		}
   204  
   205  		net, _, err := s.GetNetwork(netName)
   206  		if err != nil {
   207  			return errors.Annotatef(err, "retrieving network %q", netName)
   208  		}
   209  		if err := verifyNoIPv6(net); err != nil {
   210  			ipV6ErrMsg = err
   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  	// A nic with valid type found, but the network configures IPv6.
   220  	if ipV6ErrMsg != nil {
   221  		return ipV6ErrMsg
   222  	}
   223  
   224  	// No nics with a nictype of nicTypeBridged, nicTypeMACVLAN was found.
   225  	return errors.Errorf(fmt.Sprintf(
   226  		"no network device found with nictype %q or %q"+
   227  			"\n\tthe following devices were checked: %s"+
   228  			"\nNote: juju does not support IPv6."+
   229  			"\nReconfigure lxd to use a network of type %q or %q, disabling IPv6.",
   230  		nicTypeBridged, nicTypeMACVLAN, strings.Join(checked, ", "), nicTypeBridged, nicTypeMACVLAN))
   231  }
   232  
   233  // verifyNICsWithConfigFile is recruited for legacy LXD installations.
   234  // It checks the LXD bridge configuration file and ensure that one of the input
   235  // devices is suitable for LXD to work with Juju.
   236  func (s *Server) verifyNICsWithConfigFile(nics map[string]device, reader func(string) ([]byte, error)) error {
   237  	netName, err := checkBridgeConfigFile(reader)
   238  	if err != nil {
   239  		return errors.Trace(err)
   240  	}
   241  
   242  	checked := make([]string, 0, len(nics))
   243  	for name, nic := range nics {
   244  		checked = append(checked, name)
   245  
   246  		if nic["parent"] != netName {
   247  			continue
   248  		}
   249  		if !isValidNICType(nic) {
   250  			continue
   251  		}
   252  
   253  		logger.Tracef("found usable network device %q with parent %q", name, netName)
   254  		s.localBridgeName = netName
   255  		return nil
   256  	}
   257  
   258  	return errors.Errorf("no network device found with nictype %q or %q that uses the configured bridge in %s"+
   259  		"\n\tthe following devices were checked: %v", nicTypeBridged, nicTypeMACVLAN, BridgeConfigFile, checked)
   260  }
   261  
   262  // generateNICDeviceName attempts to generate a new NIC device name that is not
   263  // already in the input profile. If none can be determined in a reasonable
   264  // search space, an empty name is returned. This should never really happen,
   265  // but the name generation aborts to be safe from (theoretical) integer overflow.
   266  func generateNICDeviceName(profile *api.Profile) string {
   267  	template := "eth%d"
   268  	for i := 0; i < 100; i++ {
   269  		name := fmt.Sprintf(template, i)
   270  		unique := true
   271  		for d := range profile.Devices {
   272  			if d == name {
   273  				unique = false
   274  				break
   275  			}
   276  		}
   277  		if unique {
   278  			return name
   279  		}
   280  	}
   281  	return ""
   282  }
   283  
   284  // getProfileNICs iterates over the devices in the input profile and returns
   285  // any that are of type "nic".
   286  func getProfileNICs(profile *api.Profile) map[string]device {
   287  	nics := make(map[string]device, len(profile.Devices))
   288  	for k, v := range profile.Devices {
   289  		if v["type"] == nic {
   290  			nics[k] = v
   291  		}
   292  	}
   293  	return nics
   294  }
   295  
   296  // verifyNoIPv6 checks that the input network has no IPv6 configuration.
   297  // An error is returned when it does.
   298  // TODO (manadart 2018-05-28) The intention is to support IPv6, so this
   299  // restriction is temporary.
   300  func verifyNoIPv6(net *api.Network) error {
   301  	if !net.Managed {
   302  		return nil
   303  	}
   304  	cfg, ok := net.Config["ipv6.address"]
   305  	if !ok {
   306  		return nil
   307  	}
   308  	if cfg == "none" {
   309  		return nil
   310  	}
   311  
   312  	return errors.Errorf("juju does not support IPv6. Disable IPv6 in LXD via:\n"+
   313  		"\tlxc network set %s ipv6.address none\n"+
   314  		"and run the command again", net.Name)
   315  }
   316  
   317  func isValidNICType(nic device) bool {
   318  	return nic["nictype"] == nicTypeBridged || nic["nictype"] == nicTypeMACVLAN
   319  }
   320  
   321  const BridgeConfigFile = "/etc/default/lxd-bridge"
   322  
   323  // checkBridgeConfigFile verifies that the file configuration for the LXD
   324  // bridge has a a bridge name, that it is set to be used by LXD and that
   325  // it has (only) IPv4 configuration.
   326  // TODO (manadart 2018-05-28) The error messages are invalid for LXD
   327  // installations that pre-date the network API support and that were installed
   328  // via Snap. The question of the correct user action was posed on the #lxd IRC
   329  // channel, but has not be answered to-date.
   330  func checkBridgeConfigFile(reader func(string) ([]byte, error)) (string, error) {
   331  	bridgeConfig, err := reader(BridgeConfigFile)
   332  	if os.IsNotExist(err) {
   333  		return "", bridgeConfigError("no config file found at " + BridgeConfigFile)
   334  	} else if err != nil {
   335  		return "", errors.Trace(err)
   336  	}
   337  
   338  	foundSubnetConfig := false
   339  	bridgeName := ""
   340  	for _, line := range strings.Split(string(bridgeConfig), "\n") {
   341  		if strings.HasPrefix(line, "USE_LXD_BRIDGE=") {
   342  			b, err := strconv.ParseBool(strings.Trim(line[len("USE_LXD_BRIDGE="):], " \""))
   343  			if err != nil {
   344  				logger.Debugf("unable to parse bool, skipping USE_LXD_BRIDGE check: %s", err)
   345  				continue
   346  			}
   347  			if !b {
   348  				return "", bridgeConfigError(fmt.Sprintf("%s has USE_LXD_BRIDGE set to false", BridgeConfigFile))
   349  			}
   350  		} else if strings.HasPrefix(line, "LXD_BRIDGE=") {
   351  			bridgeName = strings.Trim(line[len("LXD_BRIDGE="):], " \"")
   352  			if bridgeName == "" {
   353  				return "", bridgeConfigError(fmt.Sprintf("%s has no LXD_BRIDGE set", BridgeConfigFile))
   354  			}
   355  		} else if strings.HasPrefix(line, "LXD_IPV4_ADDR=") {
   356  			contents := strings.Trim(line[len("LXD_IPV4_ADDR="):], " \"")
   357  			if len(contents) > 0 {
   358  				foundSubnetConfig = true
   359  			}
   360  		} else if strings.HasPrefix(line, "LXD_IPV6_ADDR=") {
   361  			contents := strings.Trim(line[len("LXD_IPV6_ADDR="):], " \"")
   362  			if len(contents) > 0 {
   363  				return "", ipv6BridgeConfigError(BridgeConfigFile)
   364  			}
   365  		}
   366  	}
   367  
   368  	if !foundSubnetConfig {
   369  		// TODO (hml) 2018-08-09 Question
   370  		// Should the error mention ipv6 is not enabled if juju doesn't support it?
   371  		return "", bridgeConfigError(bridgeName + " has no ipv4 or ipv6 subnet enabled")
   372  	}
   373  	return bridgeName, nil
   374  }
   375  
   376  func bridgeConfigError(err string) error {
   377  	return errors.Errorf("%s\nIt looks like your LXD bridge has not yet been configured. Configure it via:\n\n"+
   378  		"\tsudo dpkg-reconfigure -p medium lxd\n\n"+
   379  		"and run the command again.", err)
   380  }
   381  
   382  func ipv6BridgeConfigError(fileName string) error {
   383  	return errors.Errorf("%s has IPv6 enabled.\nJuju doesn't currently support IPv6.\n"+
   384  		"Disable IPv6 via:\n\n"+
   385  		"\tsudo dpkg-reconfigure -p medium lxd\n\n"+
   386  		"and run the command again.", fileName)
   387  }
   388  
   389  // InterfaceInfoFromDevices returns a slice of interface info congruent with the
   390  // input LXD NIC devices.
   391  // The output is used to generate cloud-init user-data congruent with the NICs
   392  // that end up in the container.
   393  func InterfaceInfoFromDevices(nics map[string]device) ([]network.InterfaceInfo, error) {
   394  	interfaces := make([]network.InterfaceInfo, len(nics))
   395  	var i int
   396  	for name, device := range nics {
   397  		iInfo := network.InterfaceInfo{
   398  			InterfaceName:       name,
   399  			ParentInterfaceName: device["parent"],
   400  			MACAddress:          device["hwaddr"],
   401  			ConfigType:          network.ConfigDHCP,
   402  		}
   403  		if device["mtu"] != "" {
   404  			mtu, err := strconv.Atoi(device["mtu"])
   405  			if err != nil {
   406  				return nil, errors.Annotate(err, "parsing device MTU")
   407  			}
   408  			iInfo.MTU = mtu
   409  		}
   410  
   411  		interfaces[i] = iInfo
   412  		i++
   413  	}
   414  
   415  	sortInterfacesByName(interfaces)
   416  	return interfaces, nil
   417  }
   418  
   419  type interfaceInfoSlice []network.InterfaceInfo
   420  
   421  func (s interfaceInfoSlice) Len() int      { return len(s) }
   422  func (s interfaceInfoSlice) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
   423  func (s interfaceInfoSlice) Less(i, j int) bool {
   424  	return s[i].InterfaceName < s[j].InterfaceName
   425  }
   426  
   427  func sortInterfacesByName(interfaces []network.InterfaceInfo) {
   428  	sort.Sort(interfaceInfoSlice(interfaces))
   429  }
   430  
   431  // errIPV6NotSupported is the error returned by glibc for attempts at
   432  // unsupported protocols.
   433  const errIPV6NotSupported = `socket: address family not supported by protocol`
   434  
   435  // DevicesFromInterfaceInfo uses the input interface info collection to create a
   436  // map of network device configuration in the LXD format.
   437  // Names for any networks without a known CIDR are returned in a slice.
   438  func DevicesFromInterfaceInfo(interfaces []network.InterfaceInfo) (map[string]device, []string, error) {
   439  	nics := make(map[string]device, len(interfaces))
   440  	var unknown []string
   441  
   442  	for _, v := range interfaces {
   443  		if v.InterfaceType == network.LoopbackInterface {
   444  			continue
   445  		}
   446  		if v.InterfaceType != network.EthernetInterface {
   447  			return nil, nil, errors.Errorf("interface type %q not supported", v.InterfaceType)
   448  		}
   449  		if v.ParentInterfaceName == "" {
   450  			return nil, nil, errors.Errorf("parent interface name is empty")
   451  		}
   452  		if v.CIDR == "" {
   453  			unknown = append(unknown, v.ParentInterfaceName)
   454  		}
   455  		nics[v.InterfaceName] = newNICDevice(v.InterfaceName, v.ParentInterfaceName, v.MACAddress, v.MTU)
   456  	}
   457  
   458  	return nics, unknown, nil
   459  }
   460  
   461  // newNICDevice creates and returns a LXD-compatible config for a network
   462  // device from the input arguments.
   463  // TODO (manadart 2018-06-21) We want to support nictype=macvlan too.
   464  // This will involve interrogating the parent device, via the server if it is
   465  // LXD managed, or via the container.NetworkConfig.DeviceType that this is
   466  // being generated from.
   467  func newNICDevice(deviceName, parentDevice, hwAddr string, mtu int) device {
   468  	device := map[string]string{
   469  		"type":    "nic",
   470  		"nictype": "bridged",
   471  		"name":    deviceName,
   472  		"parent":  parentDevice,
   473  	}
   474  	if hwAddr != "" {
   475  		device["hwaddr"] = hwAddr
   476  	}
   477  	if mtu > 0 {
   478  		device["mtu"] = fmt.Sprintf("%v", mtu)
   479  	}
   480  	return device
   481  }