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 }