github.com/makyo/juju@v0.0.0-20160425123129-2608902037e9/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/lxc/lxd"
    20  	lxdshared "github.com/lxc/lxd/shared"
    21  
    22  	"github.com/juju/errors"
    23  	"github.com/juju/loggo"
    24  )
    25  
    26  var logger = loggo.GetLogger("juju.tools.lxdclient")
    27  
    28  // lxdLogProxy proxies LXD's log calls through the juju logger so we can get
    29  // more info about what's going on.
    30  type lxdLogProxy struct {
    31  	logger loggo.Logger
    32  }
    33  
    34  func (p *lxdLogProxy) render(msg string, ctx []interface{}) string {
    35  	result := bytes.Buffer{}
    36  	result.WriteString(msg)
    37  	if len(ctx) > 0 {
    38  		result.WriteString(": ")
    39  	}
    40  
    41  	/* This is sort of a hack, but it's enforced in the LXD code itself as
    42  	 * well. LXD's logging framework forces us to pass things as "string"
    43  	 * for one argument and then a "context object" as the next argument.
    44  	 * So, we do some basic rendering here to make it look slightly less
    45  	 * ugly.
    46  	 */
    47  	var key string
    48  	for i, entry := range ctx {
    49  		if i != 0 {
    50  			result.WriteString(", ")
    51  		}
    52  
    53  		if key == "" {
    54  			key, _ = entry.(string)
    55  		} else {
    56  			result.WriteString(key)
    57  			result.WriteString(": ")
    58  			result.WriteString(fmt.Sprintf("%s", entry))
    59  			key = ""
    60  		}
    61  	}
    62  
    63  	return result.String()
    64  }
    65  
    66  func (p *lxdLogProxy) Debug(msg string, ctx ...interface{}) {
    67  	p.logger.Debugf(p.render(msg, ctx))
    68  }
    69  
    70  func (p *lxdLogProxy) Info(msg string, ctx ...interface{}) {
    71  	p.logger.Infof(p.render(msg, ctx))
    72  }
    73  
    74  func (p *lxdLogProxy) Warn(msg string, ctx ...interface{}) {
    75  	p.logger.Warningf(p.render(msg, ctx))
    76  }
    77  
    78  func (p *lxdLogProxy) Error(msg string, ctx ...interface{}) {
    79  	p.logger.Errorf(p.render(msg, ctx))
    80  }
    81  
    82  func (p *lxdLogProxy) Crit(msg string, ctx ...interface{}) {
    83  	p.logger.Criticalf(p.render(msg, ctx))
    84  }
    85  
    86  func init() {
    87  	lxdshared.Log = &lxdLogProxy{loggo.GetLogger("lxd")}
    88  }
    89  
    90  const LXDBridgeFile = "/etc/default/lxd-bridge"
    91  
    92  // Client is a high-level wrapper around the LXD API client.
    93  type Client struct {
    94  	*serverConfigClient
    95  	*certClient
    96  	*profileClient
    97  	*instanceClient
    98  	*imageClient
    99  	baseURL string
   100  }
   101  
   102  func (c Client) String() string {
   103  	return fmt.Sprintf("Client(%s)", c.baseURL)
   104  }
   105  
   106  // Connect opens an API connection to LXD and returns a high-level
   107  // Client wrapper around that connection.
   108  func Connect(cfg Config) (*Client, error) {
   109  	if err := cfg.Validate(); err != nil {
   110  		return nil, errors.Trace(err)
   111  	}
   112  
   113  	remote := cfg.Remote.ID()
   114  
   115  	raw, err := newRawClient(cfg.Remote)
   116  	if err != nil {
   117  		return nil, errors.Trace(err)
   118  	}
   119  
   120  	conn := &Client{
   121  		serverConfigClient: &serverConfigClient{raw},
   122  		certClient:         &certClient{raw},
   123  		profileClient:      &profileClient{raw},
   124  		instanceClient:     &instanceClient{raw, remote},
   125  		imageClient:        &imageClient{raw, connectToRaw},
   126  		baseURL:            raw.BaseURL,
   127  	}
   128  	return conn, nil
   129  }
   130  
   131  var lxdNewClientFromInfo = lxd.NewClientFromInfo
   132  
   133  func isSupportedLxdVersion(version string) bool {
   134  	var major, minor, micro int
   135  	var err error
   136  
   137  	versionParts := strings.Split(version, ".")
   138  	if len(versionParts) < 3 {
   139  		return false
   140  	}
   141  
   142  	major, err = strconv.Atoi(versionParts[0])
   143  	if err != nil {
   144  		return false
   145  	}
   146  
   147  	minor, err = strconv.Atoi(versionParts[1])
   148  	if err != nil {
   149  		return false
   150  	}
   151  
   152  	micro, err = strconv.Atoi(versionParts[2])
   153  	if err != nil {
   154  		return false
   155  	}
   156  
   157  	if major < 2 {
   158  		return false
   159  	}
   160  
   161  	/* disallow 2.0.0.rc4 and friends */
   162  	if major == 2 && minor == 0 && micro == 0 && len(versionParts) > 3 {
   163  		return false
   164  	}
   165  
   166  	return true
   167  }
   168  
   169  // newRawClient connects to the LXD host that is defined in Config.
   170  func newRawClient(remote Remote) (*lxd.Client, error) {
   171  	host := remote.Host
   172  	logger.Debugf("connecting to LXD remote %q: %q", remote.ID(), host)
   173  
   174  	if remote.ID() == remoteIDForLocal && host == "" {
   175  		host = "unix://" + lxdshared.VarPath("unix.socket")
   176  	} else if !strings.HasPrefix(host, "unix://") {
   177  		_, _, err := net.SplitHostPort(host)
   178  		if err != nil {
   179  			// There is no port here
   180  			host = net.JoinHostPort(host, lxdshared.DefaultPort)
   181  		}
   182  	}
   183  
   184  	clientCert := ""
   185  	if remote.Cert != nil && remote.Cert.CertPEM != nil {
   186  		clientCert = string(remote.Cert.CertPEM)
   187  	}
   188  
   189  	clientKey := ""
   190  	if remote.Cert != nil && remote.Cert.KeyPEM != nil {
   191  		clientKey = string(remote.Cert.KeyPEM)
   192  	}
   193  
   194  	static := false
   195  	public := false
   196  	if remote.Protocol == SimplestreamsProtocol {
   197  		static = true
   198  		public = true
   199  	}
   200  
   201  	client, err := lxdNewClientFromInfo(lxd.ConnectInfo{
   202  		Name: remote.ID(),
   203  		RemoteConfig: lxd.RemoteConfig{
   204  			Addr:     host,
   205  			Static:   static,
   206  			Public:   public,
   207  			Protocol: string(remote.Protocol),
   208  		},
   209  		ClientPEMCert: clientCert,
   210  		ClientPEMKey:  clientKey,
   211  		ServerPEMCert: remote.ServerPEMCert,
   212  	})
   213  	if err != nil {
   214  		if remote.ID() == remoteIDForLocal {
   215  			err = hoistLocalConnectErr(err)
   216  			return nil, errors.Annotate(err, "can't connect to the local LXD server")
   217  		}
   218  		return nil, errors.Trace(err)
   219  	}
   220  
   221  	if remote.Protocol != SimplestreamsProtocol {
   222  		status, err := client.ServerStatus()
   223  		if err != nil {
   224  			return nil, errors.Trace(err)
   225  		}
   226  
   227  		if !isSupportedLxdVersion(status.Environment.ServerVersion) {
   228  			return nil, errors.Errorf("lxd version %s, juju needs at least 2.0.0", status.Environment.ServerVersion)
   229  		}
   230  	}
   231  
   232  	/* If this is the LXD provider on the localhost, let's do an extra
   233  	 * check to make sure that lxdbr0 is configured.
   234  	 */
   235  	if remote.ID() == remoteIDForLocal {
   236  		profile, err := client.ProfileConfig("default")
   237  		if err != nil {
   238  			return nil, errors.Trace(err)
   239  		}
   240  
   241  		/* If the default profile doesn't have eth0 in it, then the
   242  		 * user has messed with it, so let's just use whatever they set up.
   243  		 *
   244  		 * Otherwise, if it looks like it's pointing at our lxdbr0,
   245  		 * let's check and make sure that is configured.
   246  		 */
   247  		eth0, ok := profile.Devices["eth0"]
   248  		if ok && eth0["type"] == "nic" && eth0["nictype"] == "bridged" && eth0["parent"] == "lxdbr0" {
   249  			conf, err := ioutil.ReadFile(LXDBridgeFile)
   250  			if err != nil {
   251  				if !os.IsNotExist(err) {
   252  					return nil, errors.Trace(err)
   253  				} else {
   254  					return nil, bridgeConfigError("lxdbr0 configured but no config file found at " + LXDBridgeFile)
   255  				}
   256  			}
   257  
   258  			if err = checkLXDBridgeConfiguration(string(conf)); err != nil {
   259  				return nil, errors.Trace(err)
   260  			}
   261  		}
   262  	}
   263  
   264  	return client, nil
   265  }
   266  
   267  func bridgeConfigError(err string) error {
   268  	return errors.Errorf(`%s
   269  It looks like your lxdbr0 has not yet been configured. Please configure it via:
   270  
   271  	sudo dpkg-reconfigure -p medium lxd
   272  
   273  and then bootstrap again.`, err)
   274  }
   275  
   276  func checkLXDBridgeConfiguration(conf string) error {
   277  	foundSubnetConfig := false
   278  	for _, line := range strings.Split(conf, "\n") {
   279  		if strings.HasPrefix(line, "USE_LXD_BRIDGE=") {
   280  			b, err := strconv.ParseBool(strings.Trim(line[len("USE_LXD_BRIDGE="):], " \""))
   281  			if err != nil {
   282  				logger.Warningf("couldn't parse bool, skipping USE_LXD_BRIDGE check: %s", err)
   283  				continue
   284  			}
   285  
   286  			if !b {
   287  				return bridgeConfigError("lxdbr0 not enabled but required")
   288  			}
   289  		} else if strings.HasPrefix(line, "LXD_BRIDGE=") {
   290  			name := strings.Trim(line[len("LXD_BRIDGE="):], " \"")
   291  			/* If we're here, we want lxdbr0 to be configured
   292  			 * because the default profile that juju will use says
   293  			 * lxdbr0. So let's fail if it doesn't.
   294  			 */
   295  			if name != "lxdbr0" {
   296  				return bridgeConfigError(fmt.Sprintf(LXDBridgeFile+" has a bridge named %s, not lxdbr0", name))
   297  			}
   298  		} else if strings.HasPrefix(line, "LXD_IPV4_ADDR=") || strings.HasPrefix(line, "LXD_IPV6_ADDR=") {
   299  			contents := strings.Trim(line[len("LXD_IPVN_ADDR="):], " \"")
   300  			if len(contents) > 0 {
   301  				foundSubnetConfig = true
   302  			}
   303  		}
   304  	}
   305  
   306  	if !foundSubnetConfig {
   307  		return bridgeConfigError("lxdbr0 has no ipv4 or ipv6 subnet enabled")
   308  	}
   309  
   310  	return nil
   311  }
   312  
   313  func getMessageFromErr(err error) (bool, string) {
   314  	msg := err.Error()
   315  	t, ok := err.(*url.Error)
   316  	if !ok {
   317  		return false, msg
   318  	}
   319  
   320  	u, ok := t.Err.(*net.OpError)
   321  	if !ok {
   322  		return false, msg
   323  	}
   324  
   325  	if u.Op == "dial" && u.Net == "unix" {
   326  		var lxdErr error
   327  
   328  		sysErr, ok := u.Err.(*os.SyscallError)
   329  		if ok {
   330  			lxdErr = sysErr.Err
   331  		} else {
   332  			// Try a syscall.Errno as that is what's returned for CentOS
   333  			errno, ok := u.Err.(syscall.Errno)
   334  			if !ok {
   335  				return false, msg
   336  			}
   337  			lxdErr = errno
   338  		}
   339  
   340  		switch lxdErr {
   341  		case syscall.ENOENT:
   342  			return false, "LXD socket not found; is LXD installed & running?"
   343  		case syscall.ECONNREFUSED:
   344  			return true, "LXD refused connections; is LXD running?"
   345  		case syscall.EACCES:
   346  			return true, "Permisson denied, are you in the lxd group?"
   347  		}
   348  	}
   349  
   350  	return false, msg
   351  }
   352  
   353  func hoistLocalConnectErr(err error) error {
   354  
   355  	installed, msg := getMessageFromErr(err)
   356  
   357  	configureText := `
   358  Please configure LXD by running:
   359  	$ newgrp lxd
   360  	$ lxd init
   361  `
   362  
   363  	installText := `
   364  Please install LXD by running:
   365  	$ sudo apt-get install lxd
   366  and then configure it with:
   367  	$ newgrp lxd
   368  	$ lxd init
   369  `
   370  
   371  	hint := installText
   372  	if installed {
   373  		hint = configureText
   374  	}
   375  
   376  	return errors.Trace(fmt.Errorf("%s\n%s", msg, hint))
   377  }