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 }