github.com/wallyworld/juju@v0.0.0-20161013125918-6cf1bc9d917a/tools/lxdclient/client.go (about)

     1  // Copyright 2015 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  // +build go1.3
     5  
     6  package lxdclient
     7  
     8  import (
     9  	"bytes"
    10  	"fmt"
    11  	"io/ioutil"
    12  	"net"
    13  	"net/url"
    14  	"os"
    15  	"strconv"
    16  	"strings"
    17  	"syscall"
    18  
    19  	"github.com/juju/errors"
    20  	"github.com/juju/loggo"
    21  	"github.com/lxc/lxd"
    22  	lxdshared "github.com/lxc/lxd/shared"
    23  
    24  	"github.com/juju/juju/network"
    25  )
    26  
    27  var logger = loggo.GetLogger("juju.tools.lxdclient")
    28  
    29  // lxdLogProxy proxies LXD's log calls through the juju logger so we can get
    30  // more info about what's going on.
    31  type lxdLogProxy struct {
    32  	logger loggo.Logger
    33  }
    34  
    35  func (p *lxdLogProxy) render(msg string, ctx []interface{}) string {
    36  	result := bytes.Buffer{}
    37  	result.WriteString(msg)
    38  	if len(ctx) > 0 {
    39  		result.WriteString(": ")
    40  	}
    41  
    42  	/* This is sort of a hack, but it's enforced in the LXD code itself as
    43  	 * well. LXD's logging framework forces us to pass things as "string"
    44  	 * for one argument and then a "context object" as the next argument.
    45  	 * So, we do some basic rendering here to make it look slightly less
    46  	 * ugly.
    47  	 */
    48  	var key string
    49  	for i, entry := range ctx {
    50  		if i != 0 {
    51  			result.WriteString(", ")
    52  		}
    53  
    54  		if key == "" {
    55  			key, _ = entry.(string)
    56  		} else {
    57  			result.WriteString(key)
    58  			result.WriteString(": ")
    59  			result.WriteString(fmt.Sprintf("%s", entry))
    60  			key = ""
    61  		}
    62  	}
    63  
    64  	return result.String()
    65  }
    66  
    67  func (p *lxdLogProxy) Debug(msg string, ctx ...interface{}) {
    68  	p.logger.Debugf(p.render(msg, ctx))
    69  }
    70  
    71  func (p *lxdLogProxy) Info(msg string, ctx ...interface{}) {
    72  	p.logger.Infof(p.render(msg, ctx))
    73  }
    74  
    75  func (p *lxdLogProxy) Warn(msg string, ctx ...interface{}) {
    76  	p.logger.Warningf(p.render(msg, ctx))
    77  }
    78  
    79  func (p *lxdLogProxy) Error(msg string, ctx ...interface{}) {
    80  	p.logger.Errorf(p.render(msg, ctx))
    81  }
    82  
    83  func (p *lxdLogProxy) Crit(msg string, ctx ...interface{}) {
    84  	p.logger.Criticalf(p.render(msg, ctx))
    85  }
    86  
    87  func init() {
    88  	lxdshared.Log = &lxdLogProxy{loggo.GetLogger("lxd")}
    89  }
    90  
    91  const LXDBridgeFile = "/etc/default/lxd-bridge"
    92  
    93  // Client is a high-level wrapper around the LXD API client.
    94  type Client struct {
    95  	*serverConfigClient
    96  	*certClient
    97  	*profileClient
    98  	*instanceClient
    99  	*imageClient
   100  	*networkClient
   101  	baseURL                  string
   102  	defaultProfileBridgeName string
   103  }
   104  
   105  func (c Client) String() string {
   106  	return fmt.Sprintf("Client(%s)", c.baseURL)
   107  }
   108  
   109  func (c Client) DefaultProfileBridgeName() string {
   110  	return c.defaultProfileBridgeName
   111  }
   112  
   113  // Connect opens an API connection to LXD and returns a high-level
   114  // Client wrapper around that connection.
   115  func Connect(cfg Config, verifyBridgeConfig bool) (*Client, error) {
   116  	if err := cfg.Validate(); err != nil {
   117  		return nil, errors.Trace(err)
   118  	}
   119  
   120  	remoteID := cfg.Remote.ID()
   121  
   122  	raw, err := newRawClient(cfg.Remote)
   123  	if err != nil {
   124  		return nil, errors.Trace(err)
   125  	}
   126  
   127  	networkAPISupported := false
   128  	if cfg.Remote.Protocol != SimplestreamsProtocol {
   129  		status, err := raw.ServerStatus()
   130  		if err != nil {
   131  			return nil, errors.Trace(err)
   132  		}
   133  
   134  		if lxdshared.StringInSlice("network", status.APIExtensions) {
   135  			networkAPISupported = true
   136  		}
   137  	}
   138  
   139  	var bridgeName string
   140  	if remoteID == remoteIDForLocal && verifyBridgeConfig {
   141  		// If this is the LXD provider on the localhost, let's do an extra check to
   142  		// make sure the default profile has a correctly configured bridge, and
   143  		// which one is it.
   144  		bridgeName, err = verifyDefaultProfileBridgeConfig(raw, networkAPISupported)
   145  		if err != nil {
   146  			return nil, errors.Trace(err)
   147  		}
   148  	}
   149  
   150  	conn := &Client{
   151  		serverConfigClient:       &serverConfigClient{raw},
   152  		certClient:               &certClient{raw},
   153  		profileClient:            &profileClient{raw},
   154  		instanceClient:           &instanceClient{raw, remoteID},
   155  		imageClient:              &imageClient{raw, connectToRaw},
   156  		networkClient:            &networkClient{raw, networkAPISupported},
   157  		baseURL:                  raw.BaseURL,
   158  		defaultProfileBridgeName: bridgeName,
   159  	}
   160  	return conn, nil
   161  }
   162  
   163  var lxdNewClientFromInfo = lxd.NewClientFromInfo
   164  
   165  func isSupportedAPIVersion(version string) bool {
   166  	versionParts := strings.Split(version, ".")
   167  	if len(versionParts) < 2 {
   168  		logger.Warningf("LXD API version %q: expected format <major>.<minor>", version)
   169  		return false
   170  	}
   171  
   172  	major, err := strconv.Atoi(versionParts[0])
   173  	if err != nil {
   174  		logger.Warningf("LXD API version %q: unexpected major number: %v", version, err)
   175  		return false
   176  	}
   177  
   178  	if major < 1 {
   179  		logger.Warningf("LXD API version %q: expected major version 1 or later", version)
   180  		return false
   181  	}
   182  
   183  	return true
   184  }
   185  
   186  // newRawClient connects to the LXD host that is defined in Config.
   187  func newRawClient(remote Remote) (*lxd.Client, error) {
   188  	host := remote.Host
   189  
   190  	if remote.ID() == remoteIDForLocal && host == "" {
   191  		host = "unix://" + lxdshared.VarPath("unix.socket")
   192  	} else if !strings.HasPrefix(host, "unix://") {
   193  		_, _, err := net.SplitHostPort(host)
   194  		if err != nil {
   195  			// There is no port here
   196  			host = net.JoinHostPort(host, lxdshared.DefaultPort)
   197  		}
   198  	}
   199  	logger.Debugf("connecting to LXD remote %q: %q", remote.ID(), host)
   200  
   201  	clientCert := ""
   202  	if remote.Cert != nil && remote.Cert.CertPEM != nil {
   203  		clientCert = string(remote.Cert.CertPEM)
   204  	}
   205  
   206  	clientKey := ""
   207  	if remote.Cert != nil && remote.Cert.KeyPEM != nil {
   208  		clientKey = string(remote.Cert.KeyPEM)
   209  	}
   210  
   211  	static := false
   212  	public := false
   213  	if remote.Protocol == SimplestreamsProtocol {
   214  		static = true
   215  		public = true
   216  	}
   217  
   218  	client, err := lxdNewClientFromInfo(lxd.ConnectInfo{
   219  		Name: remote.ID(),
   220  		RemoteConfig: lxd.RemoteConfig{
   221  			Addr:     host,
   222  			Static:   static,
   223  			Public:   public,
   224  			Protocol: string(remote.Protocol),
   225  		},
   226  		ClientPEMCert: clientCert,
   227  		ClientPEMKey:  clientKey,
   228  		ServerPEMCert: remote.ServerPEMCert,
   229  	})
   230  	if err != nil {
   231  		if remote.ID() == remoteIDForLocal {
   232  			err = hoistLocalConnectErr(err)
   233  			return nil, errors.Annotate(err, "can't connect to the local LXD server")
   234  		}
   235  		return nil, errors.Trace(err)
   236  	}
   237  
   238  	if remote.Protocol != SimplestreamsProtocol {
   239  		status, err := client.ServerStatus()
   240  		if err != nil {
   241  			return nil, errors.Trace(err)
   242  		}
   243  
   244  		if !isSupportedAPIVersion(status.APIVersion) {
   245  			logger.Warningf("trying to use unsupported LXD API version %q", status.APIVersion)
   246  		} else {
   247  			logger.Infof("using LXD API version %q", status.APIVersion)
   248  		}
   249  	}
   250  
   251  	return client, nil
   252  }
   253  
   254  // verifyDefaultProfileBridgeConfig takes a LXD API client and extracts the
   255  // network bridge configured on the "default" profile. Additionally, if the
   256  // default bridge bridge is used, its configuration in LXDBridgeFile is also
   257  // inspected to make sure it has a chance to work.
   258  func verifyDefaultProfileBridgeConfig(client *lxd.Client, networkAPISupported bool) (string, error) {
   259  	const (
   260  		defaultProfileName = "default"
   261  		configTypeKey      = "type"
   262  		configTypeNic      = "nic"
   263  		configNicTypeKey   = "nictype"
   264  		configBridged      = "bridged"
   265  		configEth0         = "eth0"
   266  		configParentKey    = "parent"
   267  	)
   268  
   269  	config, err := client.ProfileConfig(defaultProfileName)
   270  	if err != nil {
   271  		return "", errors.Trace(err)
   272  	}
   273  
   274  	eth0, ok := config.Devices[configEth0]
   275  	if !ok {
   276  		/* on lxd >= 2.3, there is nothing in the default profile
   277  		 * w.r.t. eth0, because there is no lxdbr0 by default. Let's
   278  		 * handle this case and configure one now.
   279  		 */
   280  		if networkAPISupported {
   281  			if err := CreateDefaultBridgeInDefaultProfile(client); err != nil {
   282  				return "", errors.Annotate(err, "couldn't create default bridge")
   283  			}
   284  
   285  			return network.DefaultLXDBridge, nil
   286  		}
   287  		return "", errors.Errorf("unexpected LXD %q profile config without eth0: %+v", defaultProfileName, config)
   288  	} else if networkAPISupported {
   289  		if err := checkBridgeConfig(client, eth0[configParentKey]); err != nil {
   290  			return "", err
   291  		}
   292  	}
   293  
   294  	// If eth0 is there, but not with the expected attributes, likewise fail
   295  	// early.
   296  	if eth0[configTypeKey] != configTypeNic || eth0[configNicTypeKey] != configBridged {
   297  		return "", errors.Errorf("unexpected LXD %q profile config: %+v", defaultProfileName, config)
   298  	}
   299  
   300  	bridgeName := eth0[configParentKey]
   301  	logger.Infof(`LXD "default" profile uses network bridge %q`, bridgeName)
   302  
   303  	if bridgeName != network.DefaultLXDBridge {
   304  		// When the user changed which bridge to use, just return its name and
   305  		// check no further.
   306  		return bridgeName, nil
   307  	}
   308  
   309  	/* if the network API is supported, that means the lxd-bridge config
   310  	 * file has been obsoleted so we don't need to check it for correctness
   311  	 */
   312  	if networkAPISupported {
   313  		return bridgeName, nil
   314  	}
   315  
   316  	bridgeConfig, err := ioutil.ReadFile(LXDBridgeFile)
   317  	if os.IsNotExist(err) {
   318  		return "", bridgeConfigError("lxdbr0 configured but no config file found at " + LXDBridgeFile)
   319  	} else if err != nil {
   320  		return "", errors.Trace(err)
   321  	}
   322  
   323  	if err := checkLXDBridgeConfiguration(string(bridgeConfig)); err != nil {
   324  		return "", errors.Trace(err)
   325  	}
   326  
   327  	return bridgeName, nil
   328  }
   329  
   330  func bridgeConfigError(err string) error {
   331  	return errors.Errorf(`%s
   332  It looks like your lxdbr0 has not yet been configured. Please configure it via:
   333  
   334  	sudo dpkg-reconfigure -p medium lxd
   335  
   336  and then bootstrap again.`, err)
   337  }
   338  
   339  func ipv6BridgeConfigError(filename string) error {
   340  	return errors.Errorf(`%s has IPv6 enabled.
   341  Juju doesn't currently support IPv6.
   342  
   343  IPv6 can be disabled by running:
   344  
   345         sudo dpkg-reconfigure -p medium lxd
   346  
   347  and then bootstrap again.`, filename)
   348  }
   349  
   350  func checkLXDBridgeConfiguration(conf string) error {
   351  	foundSubnetConfig := false
   352  	for _, line := range strings.Split(conf, "\n") {
   353  		if strings.HasPrefix(line, "USE_LXD_BRIDGE=") {
   354  			b, err := strconv.ParseBool(strings.Trim(line[len("USE_LXD_BRIDGE="):], " \""))
   355  			if err != nil {
   356  				logger.Debugf("couldn't parse bool, skipping USE_LXD_BRIDGE check: %s", err)
   357  				continue
   358  			}
   359  
   360  			if !b {
   361  				return bridgeConfigError("lxdbr0 not enabled but required")
   362  			}
   363  		} else if strings.HasPrefix(line, "LXD_BRIDGE=") {
   364  			name := strings.Trim(line[len("LXD_BRIDGE="):], " \"")
   365  			/* If we're here, we want lxdbr0 to be configured
   366  			 * because the default profile that juju will use says
   367  			 * lxdbr0. So let's fail if it doesn't.
   368  			 */
   369  			if name != network.DefaultLXDBridge {
   370  				return bridgeConfigError(fmt.Sprintf(LXDBridgeFile+" has a bridge named %s, not lxdbr0", name))
   371  			}
   372  		} else if strings.HasPrefix(line, "LXD_IPV4_ADDR=") {
   373  			contents := strings.Trim(line[len("LXD_IPV4_ADDR="):], " \"")
   374  			if len(contents) > 0 {
   375  				foundSubnetConfig = true
   376  			}
   377  		} else if strings.HasPrefix(line, "LXD_IPV6_ADDR=") {
   378  			contents := strings.Trim(line[len("LXD_IPV6_ADDR="):], " \"")
   379  			if len(contents) > 0 {
   380  				return ipv6BridgeConfigError(LXDBridgeFile)
   381  			}
   382  		}
   383  	}
   384  
   385  	if !foundSubnetConfig {
   386  		return bridgeConfigError("lxdbr0 has no ipv4 or ipv6 subnet enabled")
   387  	}
   388  
   389  	return nil
   390  }
   391  
   392  func getMessageFromErr(err error) (bool, string) {
   393  	msg := err.Error()
   394  	t, ok := err.(*url.Error)
   395  	if !ok {
   396  		return false, msg
   397  	}
   398  
   399  	u, ok := t.Err.(*net.OpError)
   400  	if !ok {
   401  		return false, msg
   402  	}
   403  
   404  	if u.Op == "dial" && u.Net == "unix" {
   405  		var lxdErr error
   406  
   407  		sysErr, ok := u.Err.(*os.SyscallError)
   408  		if ok {
   409  			lxdErr = sysErr.Err
   410  		} else {
   411  			// Try a syscall.Errno as that is what's returned for CentOS
   412  			errno, ok := u.Err.(syscall.Errno)
   413  			if !ok {
   414  				return false, msg
   415  			}
   416  			lxdErr = errno
   417  		}
   418  
   419  		switch lxdErr {
   420  		case syscall.ENOENT:
   421  			return false, "LXD socket not found; is LXD installed & running?"
   422  		case syscall.ECONNREFUSED:
   423  			return true, "LXD refused connections; is LXD running?"
   424  		case syscall.EACCES:
   425  			return true, "Permisson denied, are you in the lxd group?"
   426  		}
   427  	}
   428  
   429  	return false, msg
   430  }
   431  
   432  func hoistLocalConnectErr(err error) error {
   433  
   434  	installed, msg := getMessageFromErr(err)
   435  
   436  	configureText := `
   437  Please configure LXD by running:
   438  	$ newgrp lxd
   439  	$ lxd init
   440  `
   441  
   442  	installText := `
   443  Please install LXD by running:
   444  	$ sudo apt-get install lxd
   445  and then configure it with:
   446  	$ newgrp lxd
   447  	$ lxd init
   448  `
   449  
   450  	hint := installText
   451  	if installed {
   452  		hint = configureText
   453  	}
   454  
   455  	return errors.Trace(fmt.Errorf("%s\n%s", msg, hint))
   456  }