github.com/mhilton/juju-juju@v0.0.0-20150901100907-a94dd2c73455/provider/maas/environ.go (about) 1 // Copyright 2013 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 4 package maas 5 6 import ( 7 "bytes" 8 "encoding/base64" 9 "encoding/xml" 10 "fmt" 11 "net" 12 "net/http" 13 "net/url" 14 "strconv" 15 "strings" 16 "sync" 17 "text/template" 18 "time" 19 20 "github.com/juju/errors" 21 "github.com/juju/utils" 22 "github.com/juju/utils/set" 23 "gopkg.in/mgo.v2/bson" 24 "launchpad.net/gomaasapi" 25 26 "github.com/juju/juju/agent" 27 "github.com/juju/juju/cloudconfig/cloudinit" 28 "github.com/juju/juju/cloudconfig/instancecfg" 29 "github.com/juju/juju/cloudconfig/providerinit" 30 "github.com/juju/juju/constraints" 31 "github.com/juju/juju/environs" 32 "github.com/juju/juju/environs/config" 33 "github.com/juju/juju/environs/storage" 34 "github.com/juju/juju/instance" 35 "github.com/juju/juju/network" 36 "github.com/juju/juju/provider/common" 37 "github.com/juju/juju/state/multiwatcher" 38 "github.com/juju/juju/tools" 39 "github.com/juju/juju/version" 40 "github.com/juju/names" 41 ) 42 43 const ( 44 // We're using v1.0 of the MAAS API. 45 apiVersion = "1.0" 46 ) 47 48 // A request may fail to due "eventual consistency" semantics, which 49 // should resolve fairly quickly. A request may also fail due to a slow 50 // state transition (for instance an instance taking a while to release 51 // a security group after termination). The former failure mode is 52 // dealt with by shortAttempt, the latter by LongAttempt. 53 var shortAttempt = utils.AttemptStrategy{ 54 Total: 5 * time.Second, 55 Delay: 200 * time.Millisecond, 56 } 57 58 var ( 59 ReleaseNodes = releaseNodes 60 ReserveIPAddress = reserveIPAddress 61 ReserveIPAddressOnDevice = reserveIPAddressOnDevice 62 ReleaseIPAddress = releaseIPAddress 63 DeploymentStatusCall = deploymentStatusCall 64 ) 65 66 func releaseNodes(nodes gomaasapi.MAASObject, ids url.Values) error { 67 _, err := nodes.CallPost("release", ids) 68 return err 69 } 70 71 func reserveIPAddress(ipaddresses gomaasapi.MAASObject, cidr string, addr network.Address) error { 72 params := url.Values{} 73 params.Add("network", cidr) 74 params.Add("requested_address", addr.Value) 75 _, err := ipaddresses.CallPost("reserve", params) 76 return err 77 } 78 79 func reserveIPAddressOnDevice(devices gomaasapi.MAASObject, deviceId string, addr network.Address) error { 80 device := devices.GetSubObject(deviceId) 81 params := url.Values{} 82 params.Add("requested_address", addr.Value) 83 _, err := device.CallPost("claim_sticky_ip_address", params) 84 return err 85 86 } 87 88 func releaseIPAddress(ipaddresses gomaasapi.MAASObject, addr network.Address) error { 89 params := url.Values{} 90 params.Add("ip", addr.Value) 91 _, err := ipaddresses.CallPost("release", params) 92 return err 93 } 94 95 type maasEnviron struct { 96 common.SupportsUnitPlacementPolicy 97 98 name string 99 100 // archMutex gates access to supportedArchitectures 101 archMutex sync.Mutex 102 // supportedArchitectures caches the architectures 103 // for which images can be instantiated. 104 supportedArchitectures []string 105 106 // ecfgMutex protects the *Unlocked fields below. 107 ecfgMutex sync.Mutex 108 109 ecfgUnlocked *maasEnvironConfig 110 maasClientUnlocked *gomaasapi.MAASObject 111 storageUnlocked storage.Storage 112 113 availabilityZonesMutex sync.Mutex 114 availabilityZones []common.AvailabilityZone 115 } 116 117 var _ environs.Environ = (*maasEnviron)(nil) 118 119 func NewEnviron(cfg *config.Config) (*maasEnviron, error) { 120 env := new(maasEnviron) 121 err := env.SetConfig(cfg) 122 if err != nil { 123 return nil, err 124 } 125 env.name = cfg.Name() 126 env.storageUnlocked = NewStorage(env) 127 return env, nil 128 } 129 130 // Bootstrap is specified in the Environ interface. 131 func (env *maasEnviron) Bootstrap(ctx environs.BootstrapContext, args environs.BootstrapParams) (arch, series string, _ environs.BootstrapFinalizer, _ error) { 132 if !environs.AddressAllocationEnabled() { 133 // When address allocation is not enabled, we should use the 134 // default bridge for both LXC and KVM containers. The bridge 135 // is created as part of the userdata for every node during 136 // StartInstance. 137 logger.Infof( 138 "address allocation feature disabled; using %q bridge for all containers", 139 instancecfg.DefaultBridgeName, 140 ) 141 args.ContainerBridgeName = instancecfg.DefaultBridgeName 142 } else { 143 logger.Debugf( 144 "address allocation feature enabled; using static IPs for containers: %q", 145 instancecfg.DefaultBridgeName, 146 ) 147 } 148 149 result, series, finalizer, err := common.BootstrapInstance(ctx, env, args) 150 if err != nil { 151 return "", "", nil, err 152 } 153 154 // We want to destroy the started instance if it doesn't transition to Deployed. 155 defer func() { 156 if err != nil { 157 if err := env.StopInstances(result.Instance.Id()); err != nil { 158 logger.Errorf("error releasing bootstrap instance: %v", err) 159 } 160 } 161 }() 162 // Wait for bootstrap instance to change to deployed state. 163 if err := env.waitForNodeDeployment(result.Instance.Id()); err != nil { 164 return "", "", nil, errors.Annotate(err, "bootstrap instance started but did not change to Deployed state") 165 } 166 return *result.Hardware.Arch, series, finalizer, nil 167 } 168 169 // StateServerInstances is specified in the Environ interface. 170 func (env *maasEnviron) StateServerInstances() ([]instance.Id, error) { 171 return common.ProviderStateInstances(env, env.Storage()) 172 } 173 174 // ecfg returns the environment's maasEnvironConfig, and protects it with a 175 // mutex. 176 func (env *maasEnviron) ecfg() *maasEnvironConfig { 177 env.ecfgMutex.Lock() 178 defer env.ecfgMutex.Unlock() 179 return env.ecfgUnlocked 180 } 181 182 // Config is specified in the Environ interface. 183 func (env *maasEnviron) Config() *config.Config { 184 return env.ecfg().Config 185 } 186 187 // SetConfig is specified in the Environ interface. 188 func (env *maasEnviron) SetConfig(cfg *config.Config) error { 189 env.ecfgMutex.Lock() 190 defer env.ecfgMutex.Unlock() 191 192 // The new config has already been validated by itself, but now we 193 // validate the transition from the old config to the new. 194 var oldCfg *config.Config 195 if env.ecfgUnlocked != nil { 196 oldCfg = env.ecfgUnlocked.Config 197 } 198 cfg, err := env.Provider().Validate(cfg, oldCfg) 199 if err != nil { 200 return err 201 } 202 203 ecfg, err := providerInstance.newConfig(cfg) 204 if err != nil { 205 return err 206 } 207 208 env.ecfgUnlocked = ecfg 209 210 authClient, err := gomaasapi.NewAuthenticatedClient(ecfg.maasServer(), ecfg.maasOAuth(), apiVersion) 211 if err != nil { 212 return err 213 } 214 env.maasClientUnlocked = gomaasapi.NewMAAS(*authClient) 215 216 return nil 217 } 218 219 // SupportedArchitectures is specified on the EnvironCapability interface. 220 func (env *maasEnviron) SupportedArchitectures() ([]string, error) { 221 env.archMutex.Lock() 222 defer env.archMutex.Unlock() 223 if env.supportedArchitectures != nil { 224 return env.supportedArchitectures, nil 225 } 226 bootImages, err := env.allBootImages() 227 if err != nil || len(bootImages) == 0 { 228 logger.Debugf("error querying boot-images: %v", err) 229 logger.Debugf("falling back to listing nodes") 230 supportedArchitectures, err := env.nodeArchitectures() 231 if err != nil { 232 return nil, err 233 } 234 env.supportedArchitectures = supportedArchitectures 235 } else { 236 architectures := make(set.Strings) 237 for _, image := range bootImages { 238 architectures.Add(image.architecture) 239 } 240 env.supportedArchitectures = architectures.SortedValues() 241 } 242 return env.supportedArchitectures, nil 243 } 244 245 // SupportsAddressAllocation is specified on environs.Networking. 246 func (env *maasEnviron) SupportsAddressAllocation(_ network.Id) (bool, error) { 247 if !environs.AddressAllocationEnabled() { 248 return false, errors.NotSupportedf("address allocation") 249 } 250 251 caps, err := env.getCapabilities() 252 if err != nil { 253 return false, errors.Annotatef(err, "getCapabilities failed") 254 } 255 return caps.Contains(capStaticIPAddresses), nil 256 } 257 258 // allBootImages queries MAAS for all of the boot-images across 259 // all registered nodegroups. 260 func (env *maasEnviron) allBootImages() ([]bootImage, error) { 261 nodegroups, err := env.getNodegroups() 262 if err != nil { 263 return nil, err 264 } 265 var allBootImages []bootImage 266 seen := make(set.Strings) 267 for _, nodegroup := range nodegroups { 268 bootImages, err := env.nodegroupBootImages(nodegroup) 269 if err != nil { 270 return nil, errors.Annotatef(err, "cannot get boot images for nodegroup %v", nodegroup) 271 } 272 for _, image := range bootImages { 273 str := fmt.Sprint(image) 274 if seen.Contains(str) { 275 continue 276 } 277 seen.Add(str) 278 allBootImages = append(allBootImages, image) 279 } 280 } 281 return allBootImages, nil 282 } 283 284 // getNodegroups returns the UUID corresponding to each nodegroup 285 // in the MAAS installation. 286 func (env *maasEnviron) getNodegroups() ([]string, error) { 287 nodegroupsListing := env.getMAASClient().GetSubObject("nodegroups") 288 nodegroupsResult, err := nodegroupsListing.CallGet("list", nil) 289 if err != nil { 290 return nil, err 291 } 292 list, err := nodegroupsResult.GetArray() 293 if err != nil { 294 return nil, err 295 } 296 nodegroups := make([]string, len(list)) 297 for i, obj := range list { 298 nodegroup, err := obj.GetMap() 299 if err != nil { 300 return nil, err 301 } 302 uuid, err := nodegroup["uuid"].GetString() 303 if err != nil { 304 return nil, err 305 } 306 nodegroups[i] = uuid 307 } 308 return nodegroups, nil 309 } 310 311 func (env *maasEnviron) getNodegroupInterfaces(nodegroups []string) map[string][]net.IP { 312 nodegroupsObject := env.getMAASClient().GetSubObject("nodegroups") 313 314 nodegroupsInterfacesMap := make(map[string][]net.IP) 315 for _, uuid := range nodegroups { 316 interfacesObject := nodegroupsObject.GetSubObject(uuid).GetSubObject("interfaces") 317 interfacesResult, err := interfacesObject.CallGet("list", nil) 318 if err != nil { 319 logger.Debugf("cannot list interfaces for nodegroup %v: %v", uuid, err) 320 continue 321 } 322 interfaces, err := interfacesResult.GetArray() 323 if err != nil { 324 logger.Debugf("cannot get interfaces for nodegroup %v: %v", uuid, err) 325 continue 326 } 327 for _, interfaceResult := range interfaces { 328 nic, err := interfaceResult.GetMap() 329 if err != nil { 330 logger.Debugf("cannot get interface %v for nodegroup %v: %v", nic, uuid, err) 331 continue 332 } 333 ip, err := nic["ip"].GetString() 334 if err != nil { 335 logger.Debugf("cannot get interface IP %v for nodegroup %v: %v", nic, uuid, err) 336 continue 337 } 338 static_low, err := nic["static_ip_range_low"].GetString() 339 if err != nil { 340 logger.Debugf("cannot get static IP range lower bound for interface %v on nodegroup %v: %v", nic, uuid, err) 341 continue 342 } 343 static_high, err := nic["static_ip_range_high"].GetString() 344 if err != nil { 345 logger.Infof("cannot get static IP range higher bound for interface %v on nodegroup %v: %v", nic, uuid, err) 346 continue 347 } 348 static_low_ip := net.ParseIP(static_low) 349 static_high_ip := net.ParseIP(static_high) 350 if static_low_ip == nil || static_high_ip == nil { 351 logger.Debugf("invalid IP in static range for interface %v on nodegroup %v: %q %q", nic, uuid, static_low_ip, static_high_ip) 352 continue 353 } 354 nodegroupsInterfacesMap[ip] = []net.IP{static_low_ip, static_high_ip} 355 } 356 } 357 return nodegroupsInterfacesMap 358 } 359 360 type bootImage struct { 361 architecture string 362 release string 363 } 364 365 // nodegroupBootImages returns the set of boot-images for the specified nodegroup. 366 func (env *maasEnviron) nodegroupBootImages(nodegroupUUID string) ([]bootImage, error) { 367 nodegroupObject := env.getMAASClient().GetSubObject("nodegroups").GetSubObject(nodegroupUUID) 368 bootImagesObject := nodegroupObject.GetSubObject("boot-images/") 369 result, err := bootImagesObject.CallGet("", nil) 370 if err != nil { 371 return nil, err 372 } 373 list, err := result.GetArray() 374 if err != nil { 375 return nil, err 376 } 377 var bootImages []bootImage 378 for _, obj := range list { 379 bootimage, err := obj.GetMap() 380 if err != nil { 381 return nil, err 382 } 383 arch, err := bootimage["architecture"].GetString() 384 if err != nil { 385 return nil, err 386 } 387 release, err := bootimage["release"].GetString() 388 if err != nil { 389 return nil, err 390 } 391 bootImages = append(bootImages, bootImage{ 392 architecture: arch, 393 release: release, 394 }) 395 } 396 return bootImages, nil 397 } 398 399 // nodeArchitectures returns the architectures of all 400 // available nodes in the system. 401 // 402 // Note: this should only be used if we cannot query 403 // boot-images. 404 func (env *maasEnviron) nodeArchitectures() ([]string, error) { 405 filter := make(url.Values) 406 filter.Add("status", gomaasapi.NodeStatusDeclared) 407 filter.Add("status", gomaasapi.NodeStatusCommissioning) 408 filter.Add("status", gomaasapi.NodeStatusReady) 409 filter.Add("status", gomaasapi.NodeStatusReserved) 410 filter.Add("status", gomaasapi.NodeStatusAllocated) 411 allInstances, err := env.instances(filter) 412 if err != nil { 413 return nil, err 414 } 415 architectures := make(set.Strings) 416 for _, inst := range allInstances { 417 inst := inst.(*maasInstance) 418 arch, _, err := inst.architecture() 419 if err != nil { 420 return nil, err 421 } 422 architectures.Add(arch) 423 } 424 // TODO(dfc) why is this sorted 425 return architectures.SortedValues(), nil 426 } 427 428 type maasAvailabilityZone struct { 429 name string 430 } 431 432 func (z maasAvailabilityZone) Name() string { 433 return z.name 434 } 435 436 func (z maasAvailabilityZone) Available() bool { 437 // MAAS' physical zone attributes only include name and description; 438 // there is no concept of availability. 439 return true 440 } 441 442 // AvailabilityZones returns a slice of availability zones 443 // for the configured region. 444 func (e *maasEnviron) AvailabilityZones() ([]common.AvailabilityZone, error) { 445 e.availabilityZonesMutex.Lock() 446 defer e.availabilityZonesMutex.Unlock() 447 if e.availabilityZones == nil { 448 zonesObject := e.getMAASClient().GetSubObject("zones") 449 result, err := zonesObject.CallGet("", nil) 450 if err, ok := err.(gomaasapi.ServerError); ok && err.StatusCode == http.StatusNotFound { 451 return nil, errors.NewNotImplemented(nil, "the MAAS server does not support zones") 452 } 453 if err != nil { 454 return nil, errors.Annotate(err, "cannot query ") 455 } 456 list, err := result.GetArray() 457 if err != nil { 458 return nil, err 459 } 460 logger.Debugf("availability zones: %+v", list) 461 availabilityZones := make([]common.AvailabilityZone, len(list)) 462 for i, obj := range list { 463 zone, err := obj.GetMap() 464 if err != nil { 465 return nil, err 466 } 467 name, err := zone["name"].GetString() 468 if err != nil { 469 return nil, err 470 } 471 availabilityZones[i] = maasAvailabilityZone{name} 472 } 473 e.availabilityZones = availabilityZones 474 } 475 return e.availabilityZones, nil 476 } 477 478 // InstanceAvailabilityZoneNames returns the availability zone names for each 479 // of the specified instances. 480 func (e *maasEnviron) InstanceAvailabilityZoneNames(ids []instance.Id) ([]string, error) { 481 instances, err := e.Instances(ids) 482 if err != nil && err != environs.ErrPartialInstances { 483 return nil, err 484 } 485 zones := make([]string, len(instances)) 486 for i, inst := range instances { 487 if inst == nil { 488 continue 489 } 490 zones[i] = inst.(*maasInstance).zone() 491 } 492 return zones, nil 493 } 494 495 type maasPlacement struct { 496 nodeName string 497 zoneName string 498 } 499 500 func (e *maasEnviron) parsePlacement(placement string) (*maasPlacement, error) { 501 pos := strings.IndexRune(placement, '=') 502 if pos == -1 { 503 // If there's no '=' delimiter, assume it's a node name. 504 return &maasPlacement{nodeName: placement}, nil 505 } 506 switch key, value := placement[:pos], placement[pos+1:]; key { 507 case "zone": 508 availabilityZone := value 509 zones, err := e.AvailabilityZones() 510 if err != nil { 511 return nil, err 512 } 513 for _, z := range zones { 514 if z.Name() == availabilityZone { 515 return &maasPlacement{zoneName: availabilityZone}, nil 516 } 517 } 518 return nil, errors.Errorf("invalid availability zone %q", availabilityZone) 519 } 520 return nil, errors.Errorf("unknown placement directive: %v", placement) 521 } 522 523 func (env *maasEnviron) PrecheckInstance(series string, cons constraints.Value, placement string) error { 524 if placement == "" { 525 return nil 526 } 527 _, err := env.parsePlacement(placement) 528 return err 529 } 530 531 const ( 532 capNetworksManagement = "networks-management" 533 capStaticIPAddresses = "static-ipaddresses" 534 capDevices = "devices-management" 535 ) 536 537 func (env *maasEnviron) supportsDevices() (bool, error) { 538 caps, err := env.getCapabilities() 539 if err != nil { 540 return false, errors.Trace(err) 541 } 542 return caps.Contains(capDevices), nil 543 } 544 545 // getCapabilities asks the MAAS server for its capabilities, if 546 // supported by the server. 547 func (env *maasEnviron) getCapabilities() (set.Strings, error) { 548 caps := make(set.Strings) 549 var result gomaasapi.JSONObject 550 var err error 551 552 for a := shortAttempt.Start(); a.Next(); { 553 client := env.getMAASClient().GetSubObject("version/") 554 result, err = client.CallGet("", nil) 555 if err != nil { 556 if err, ok := err.(gomaasapi.ServerError); ok && err.StatusCode == 404 { 557 return caps, fmt.Errorf("MAAS does not support version info") 558 } 559 } else { 560 break 561 } 562 } 563 if err != nil { 564 return caps, err 565 } 566 info, err := result.GetMap() 567 if err != nil { 568 return caps, err 569 } 570 capsObj, ok := info["capabilities"] 571 if !ok { 572 return caps, fmt.Errorf("MAAS does not report capabilities") 573 } 574 items, err := capsObj.GetArray() 575 if err != nil { 576 return caps, err 577 } 578 for _, item := range items { 579 val, err := item.GetString() 580 if err != nil { 581 return set.NewStrings(), err 582 } 583 caps.Add(val) 584 } 585 return caps, nil 586 } 587 588 // getMAASClient returns a MAAS client object to use for a request, in a 589 // lock-protected fashion. 590 func (env *maasEnviron) getMAASClient() *gomaasapi.MAASObject { 591 env.ecfgMutex.Lock() 592 defer env.ecfgMutex.Unlock() 593 594 return env.maasClientUnlocked 595 } 596 597 // convertConstraints converts the given constraints into an url.Values 598 // object suitable to pass to MAAS when acquiring a node. 599 // CpuPower is ignored because it cannot translated into something 600 // meaningful for MAAS right now. 601 func convertConstraints(cons constraints.Value) url.Values { 602 params := url.Values{} 603 if cons.Arch != nil { 604 // Note: Juju and MAAS use the same architecture names. 605 // MAAS also accepts a subarchitecture (e.g. "highbank" 606 // for ARM), which defaults to "generic" if unspecified. 607 params.Add("arch", *cons.Arch) 608 } 609 if cons.CpuCores != nil { 610 params.Add("cpu_count", fmt.Sprintf("%d", *cons.CpuCores)) 611 } 612 if cons.Mem != nil { 613 params.Add("mem", fmt.Sprintf("%d", *cons.Mem)) 614 } 615 if cons.Tags != nil && len(*cons.Tags) > 0 { 616 tags, notTags := parseTags(*cons.Tags) 617 if len(tags) > 0 { 618 params.Add("tags", strings.Join(tags, ",")) 619 } 620 if len(notTags) > 0 { 621 params.Add("not_tags", strings.Join(notTags, ",")) 622 } 623 } 624 if cons.CpuPower != nil { 625 logger.Warningf("ignoring unsupported constraint 'cpu-power'") 626 } 627 return params 628 } 629 630 // parseTags parses a tags constraints, splitting it into a positive 631 // and negative tags to pass to MAAS. Positive tags have no prefix, 632 // negative tags have a "^" prefix. All spaces inside the rawTags are 633 // stripped before parsing. 634 func parseTags(rawTags []string) (tags, notTags []string) { 635 for _, tag := range rawTags { 636 tag = strings.Replace(tag, " ", "", -1) 637 if len(tag) == 0 { 638 continue 639 } 640 if strings.HasPrefix(tag, "^") { 641 notTags = append(notTags, strings.TrimPrefix(tag, "^")) 642 } else { 643 tags = append(tags, tag) 644 } 645 } 646 return tags, notTags 647 } 648 649 // addNetworks converts networks include/exclude information into 650 // url.Values object suitable to pass to MAAS when acquiring a node. 651 func addNetworks(params url.Values, includeNetworks, excludeNetworks []string) { 652 // Network Inclusion/Exclusion setup 653 if len(includeNetworks) > 0 { 654 for _, name := range includeNetworks { 655 params.Add("networks", name) 656 } 657 } 658 if len(excludeNetworks) > 0 { 659 for _, name := range excludeNetworks { 660 params.Add("not_networks", name) 661 } 662 } 663 } 664 665 // addVolumes converts volume information into 666 // url.Values object suitable to pass to MAAS when acquiring a node. 667 func addVolumes(params url.Values, volumes []volumeInfo) { 668 if len(volumes) == 0 { 669 return 670 } 671 // Requests for specific values are passed to the acquire URL 672 // as a storage URL parameter of the form: 673 // [volume-name:]sizeinGB[tag,...] 674 // See http://maas.ubuntu.com/docs/api.html#nodes 675 676 // eg storage=root:0(ssd),data:20(magnetic,5400rpm),45 677 makeVolumeParams := func(v volumeInfo) string { 678 var params string 679 if v.name != "" { 680 params = v.name + ":" 681 } 682 params += fmt.Sprintf("%d", v.sizeInGB) 683 if len(v.tags) > 0 { 684 params += fmt.Sprintf("(%s)", strings.Join(v.tags, ",")) 685 } 686 return params 687 } 688 var volParms []string 689 for _, v := range volumes { 690 params := makeVolumeParams(v) 691 volParms = append(volParms, params) 692 } 693 params.Add("storage", strings.Join(volParms, ",")) 694 } 695 696 // acquireNode allocates a node from the MAAS. 697 func (environ *maasEnviron) acquireNode( 698 nodeName, zoneName string, cons constraints.Value, includeNetworks, excludeNetworks []string, volumes []volumeInfo, 699 ) (gomaasapi.MAASObject, error) { 700 701 acquireParams := convertConstraints(cons) 702 addNetworks(acquireParams, includeNetworks, excludeNetworks) 703 addVolumes(acquireParams, volumes) 704 acquireParams.Add("agent_name", environ.ecfg().maasAgentName()) 705 if zoneName != "" { 706 acquireParams.Add("zone", zoneName) 707 } 708 if nodeName != "" { 709 acquireParams.Add("name", nodeName) 710 } else if cons.Arch == nil { 711 // TODO(axw) 2014-08-18 #1358219 712 // We should be requesting preferred 713 // architectures if unspecified, like 714 // in the other providers. 715 // 716 // This is slightly complicated in MAAS 717 // as there are a finite number of each 718 // architecture; preference may also 719 // conflict with other constraints, such 720 // as tags. Thus, a preference becomes a 721 // demand (which may fail) if not handled 722 // properly. 723 logger.Warningf( 724 "no architecture was specified, acquiring an arbitrary node", 725 ) 726 } 727 728 var result gomaasapi.JSONObject 729 var err error 730 for a := shortAttempt.Start(); a.Next(); { 731 client := environ.getMAASClient().GetSubObject("nodes/") 732 result, err = client.CallPost("acquire", acquireParams) 733 if err == nil { 734 break 735 } 736 } 737 if err != nil { 738 return gomaasapi.MAASObject{}, err 739 } 740 node, err := result.GetMAASObject() 741 if err != nil { 742 err := errors.Annotate(err, "unexpected result from 'acquire' on MAAS API") 743 return gomaasapi.MAASObject{}, err 744 } 745 return node, nil 746 } 747 748 // startNode installs and boots a node. 749 func (environ *maasEnviron) startNode(node gomaasapi.MAASObject, series string, userdata []byte) error { 750 userDataParam := base64.StdEncoding.EncodeToString(userdata) 751 params := url.Values{ 752 "distro_series": {series}, 753 "user_data": {userDataParam}, 754 } 755 // Initialize err to a non-nil value as a sentinel for the following 756 // loop. 757 err := fmt.Errorf("(no error)") 758 for a := shortAttempt.Start(); a.Next() && err != nil; { 759 _, err = node.CallPost("start", params) 760 } 761 return err 762 } 763 764 var unsupportedConstraints = []string{ 765 constraints.CpuPower, 766 constraints.InstanceType, 767 } 768 769 // ConstraintsValidator is defined on the Environs interface. 770 func (environ *maasEnviron) ConstraintsValidator() (constraints.Validator, error) { 771 validator := constraints.NewValidator() 772 validator.RegisterUnsupported(unsupportedConstraints) 773 supportedArches, err := environ.SupportedArchitectures() 774 if err != nil { 775 return nil, err 776 } 777 validator.RegisterVocabulary(constraints.Arch, supportedArches) 778 return validator, nil 779 } 780 781 // setupNetworks prepares a []network.InterfaceInfo for the given 782 // instance. Any networks in networksToDisable will be configured as 783 // disabled on the machine. Any disabled network interfaces (as 784 // discovered from the lshw output for the node) will stay disabled. 785 // The interface name discovered as primary is also returned. 786 func (environ *maasEnviron) setupNetworks(inst instance.Instance, networksToDisable set.Strings) ([]network.InterfaceInfo, string, error) { 787 // Get the instance network interfaces first. 788 interfaces, primaryIface, err := environ.getInstanceNetworkInterfaces(inst) 789 if err != nil { 790 return nil, "", errors.Annotatef(err, "getInstanceNetworkInterfaces failed") 791 } 792 logger.Debugf("node %q has network interfaces %v", inst.Id(), interfaces) 793 networks, err := environ.getInstanceNetworks(inst) 794 if err != nil { 795 return nil, "", errors.Annotatef(err, "getInstanceNetworks failed") 796 } 797 logger.Debugf("node %q has networks %v", inst.Id(), networks) 798 var tempInterfaceInfo []network.InterfaceInfo 799 for _, netw := range networks { 800 disabled := networksToDisable.Contains(netw.Name) 801 netCIDR := &net.IPNet{ 802 IP: net.ParseIP(netw.IP), 803 Mask: net.IPMask(net.ParseIP(netw.Mask)), 804 } 805 macs, err := environ.getNetworkMACs(netw.Name) 806 if err != nil { 807 return nil, "", errors.Annotatef(err, "getNetworkMACs failed") 808 } 809 logger.Debugf("network %q has MACs: %v", netw.Name, macs) 810 for _, mac := range macs { 811 if ifinfo, ok := interfaces[mac]; ok { 812 tempInterfaceInfo = append(tempInterfaceInfo, network.InterfaceInfo{ 813 MACAddress: mac, 814 InterfaceName: ifinfo.InterfaceName, 815 DeviceIndex: ifinfo.DeviceIndex, 816 CIDR: netCIDR.String(), 817 VLANTag: netw.VLANTag, 818 ProviderId: network.Id(netw.Name), 819 NetworkName: netw.Name, 820 Disabled: disabled || ifinfo.Disabled, 821 }) 822 } 823 } 824 } 825 // Verify we filled-in everything for all networks/interfaces 826 // and drop incomplete records. 827 var interfaceInfo []network.InterfaceInfo 828 for _, info := range tempInterfaceInfo { 829 if info.ProviderId == "" || info.NetworkName == "" || info.CIDR == "" { 830 logger.Infof("ignoring interface %q: missing subnet info", info.InterfaceName) 831 continue 832 } 833 if info.MACAddress == "" || info.InterfaceName == "" { 834 logger.Infof("ignoring subnet %q: missing interface info", info.ProviderId) 835 continue 836 } 837 interfaceInfo = append(interfaceInfo, info) 838 } 839 logger.Debugf("node %q network information: %#v", inst.Id(), interfaceInfo) 840 return interfaceInfo, primaryIface, nil 841 } 842 843 // DistributeInstances implements the state.InstanceDistributor policy. 844 func (e *maasEnviron) DistributeInstances(candidates, distributionGroup []instance.Id) ([]instance.Id, error) { 845 return common.DistributeInstances(e, candidates, distributionGroup) 846 } 847 848 var availabilityZoneAllocations = common.AvailabilityZoneAllocations 849 850 // MaintainInstance is specified in the InstanceBroker interface. 851 func (*maasEnviron) MaintainInstance(args environs.StartInstanceParams) error { 852 return nil 853 } 854 855 // StartInstance is specified in the InstanceBroker interface. 856 func (environ *maasEnviron) StartInstance(args environs.StartInstanceParams) ( 857 *environs.StartInstanceResult, error, 858 ) { 859 var availabilityZones []string 860 var nodeName string 861 if args.Placement != "" { 862 placement, err := environ.parsePlacement(args.Placement) 863 if err != nil { 864 return nil, err 865 } 866 switch { 867 case placement.zoneName != "": 868 availabilityZones = append(availabilityZones, placement.zoneName) 869 default: 870 nodeName = placement.nodeName 871 } 872 } 873 874 // If no placement is specified, then automatically spread across 875 // the known zones for optimal spread across the instance distribution 876 // group. 877 if args.Placement == "" { 878 var group []instance.Id 879 var err error 880 if args.DistributionGroup != nil { 881 group, err = args.DistributionGroup() 882 if err != nil { 883 return nil, errors.Annotate(err, "cannot get distribution group") 884 } 885 } 886 zoneInstances, err := availabilityZoneAllocations(environ, group) 887 if errors.IsNotImplemented(err) { 888 // Availability zones are an extension, so we may get a 889 // not implemented error; ignore these. 890 } else if err != nil { 891 return nil, errors.Annotate(err, "cannot get availability zone allocations") 892 } else if len(zoneInstances) > 0 { 893 for _, z := range zoneInstances { 894 availabilityZones = append(availabilityZones, z.ZoneName) 895 } 896 } 897 } 898 if len(availabilityZones) == 0 { 899 availabilityZones = []string{""} 900 } 901 902 // Networking. 903 // 904 // TODO(dimitern): Once we can get from spaces constraints to MAAS 905 // networks (or even directly to spaces), include them in the 906 // instance selection. 907 requestedNetworks := args.InstanceConfig.Networks 908 includeNetworks := append(args.Constraints.IncludeNetworks(), requestedNetworks...) 909 excludeNetworks := args.Constraints.ExcludeNetworks() 910 911 // Storage. 912 volumes, err := buildMAASVolumeParameters(args.Volumes, args.Constraints) 913 if err != nil { 914 return nil, errors.Annotate(err, "invalid volume parameters") 915 } 916 917 snArgs := selectNodeArgs{ 918 Constraints: args.Constraints, 919 AvailabilityZones: availabilityZones, 920 NodeName: nodeName, 921 IncludeNetworks: includeNetworks, 922 ExcludeNetworks: excludeNetworks, 923 Volumes: volumes, 924 } 925 node, err := environ.selectNode(snArgs) 926 if err != nil { 927 return nil, errors.Errorf("cannot run instances: %v", err) 928 } 929 930 inst := &maasInstance{maasObject: node, environ: environ} 931 defer func() { 932 if err != nil { 933 if err := environ.StopInstances(inst.Id()); err != nil { 934 logger.Errorf("error releasing failed instance: %v", err) 935 } 936 } 937 }() 938 939 hc, err := inst.hardwareCharacteristics() 940 if err != nil { 941 return nil, err 942 } 943 944 selectedTools, err := args.Tools.Match(tools.Filter{ 945 Arch: *hc.Arch, 946 }) 947 if err != nil { 948 return nil, err 949 } 950 args.InstanceConfig.Tools = selectedTools[0] 951 952 var networkInfo []network.InterfaceInfo 953 networkInfo, primaryIface, err := environ.setupNetworks(inst, set.NewStrings(excludeNetworks...)) 954 if err != nil { 955 return nil, err 956 } 957 958 hostname, err := inst.hostname() 959 if err != nil { 960 return nil, err 961 } 962 // Override the network bridge to use for both LXC and KVM 963 // containers on the new instance, if address allocation feature 964 // flag is not enabled. 965 if !environs.AddressAllocationEnabled() { 966 if args.InstanceConfig.AgentEnvironment == nil { 967 args.InstanceConfig.AgentEnvironment = make(map[string]string) 968 } 969 args.InstanceConfig.AgentEnvironment[agent.LxcBridge] = instancecfg.DefaultBridgeName 970 } 971 if err := instancecfg.FinishInstanceConfig(args.InstanceConfig, environ.Config()); err != nil { 972 return nil, err 973 } 974 series := args.InstanceConfig.Tools.Version.Series 975 976 cloudcfg, err := environ.newCloudinitConfig(hostname, primaryIface, series) 977 if err != nil { 978 return nil, err 979 } 980 userdata, err := providerinit.ComposeUserData(args.InstanceConfig, cloudcfg) 981 if err != nil { 982 msg := fmt.Errorf("could not compose userdata for bootstrap node: %v", err) 983 return nil, msg 984 } 985 logger.Debugf("maas user data; %d bytes", len(userdata)) 986 987 if err := environ.startNode(*inst.maasObject, series, userdata); err != nil { 988 return nil, err 989 } 990 logger.Debugf("started instance %q", inst.Id()) 991 992 if multiwatcher.AnyJobNeedsState(args.InstanceConfig.Jobs...) { 993 if err := common.AddStateInstance(environ.Storage(), inst.Id()); err != nil { 994 logger.Errorf("could not record instance in provider-state: %v", err) 995 } 996 } 997 998 requestedVolumes := make([]names.VolumeTag, len(args.Volumes)) 999 for i, v := range args.Volumes { 1000 requestedVolumes[i] = v.Tag 1001 } 1002 resultVolumes, resultAttachments, err := inst.volumes( 1003 names.NewMachineTag(args.InstanceConfig.MachineId), 1004 requestedVolumes, 1005 ) 1006 if err != nil { 1007 return nil, err 1008 } 1009 if len(resultVolumes) != len(requestedVolumes) { 1010 err = errors.New("the version of MAAS being used does not support Juju storage") 1011 return nil, err 1012 } 1013 1014 return &environs.StartInstanceResult{ 1015 Instance: inst, 1016 Hardware: hc, 1017 NetworkInfo: networkInfo, 1018 Volumes: resultVolumes, 1019 VolumeAttachments: resultAttachments, 1020 }, nil 1021 } 1022 1023 // Override for testing. 1024 var nodeDeploymentTimeout = func(environ *maasEnviron) time.Duration { 1025 sshTimeouts := environ.Config().BootstrapSSHOpts() 1026 return sshTimeouts.Timeout 1027 } 1028 1029 func (environ *maasEnviron) waitForNodeDeployment(id instance.Id) error { 1030 systemId := extractSystemId(id) 1031 longAttempt := utils.AttemptStrategy{ 1032 Delay: 10 * time.Second, 1033 Total: nodeDeploymentTimeout(environ), 1034 } 1035 1036 for a := longAttempt.Start(); a.Next(); { 1037 statusValues, err := environ.deploymentStatus(id) 1038 if errors.IsNotImplemented(err) { 1039 return nil 1040 } 1041 if err != nil { 1042 return errors.Trace(err) 1043 } 1044 if statusValues[systemId] == "Deployed" { 1045 return nil 1046 } 1047 if statusValues[systemId] == "Failed deployment" { 1048 return errors.Errorf("instance %q failed to deploy", id) 1049 } 1050 } 1051 return errors.Errorf("instance %q is started but not deployed", id) 1052 } 1053 1054 // deploymentStatus returns the deployment state of MAAS instances with 1055 // the specified Juju instance ids. 1056 // Note: the result is a map of MAAS systemId to state. 1057 func (environ *maasEnviron) deploymentStatus(ids ...instance.Id) (map[string]string, error) { 1058 nodesAPI := environ.getMAASClient().GetSubObject("nodes") 1059 result, err := DeploymentStatusCall(nodesAPI, ids...) 1060 if err != nil { 1061 if err, ok := err.(gomaasapi.ServerError); ok && err.StatusCode == http.StatusBadRequest { 1062 return nil, errors.NewNotImplemented(err, "deployment status") 1063 } 1064 return nil, errors.Trace(err) 1065 } 1066 resultMap, err := result.GetMap() 1067 if err != nil { 1068 return nil, errors.Trace(err) 1069 } 1070 statusValues := make(map[string]string) 1071 for systemId, jsonValue := range resultMap { 1072 status, err := jsonValue.GetString() 1073 if err != nil { 1074 return nil, errors.Trace(err) 1075 } 1076 statusValues[systemId] = status 1077 } 1078 return statusValues, nil 1079 } 1080 1081 func deploymentStatusCall(nodes gomaasapi.MAASObject, ids ...instance.Id) (gomaasapi.JSONObject, error) { 1082 filter := getSystemIdValues("nodes", ids) 1083 return nodes.CallGet("deployment_status", filter) 1084 } 1085 1086 type selectNodeArgs struct { 1087 AvailabilityZones []string 1088 NodeName string 1089 Constraints constraints.Value 1090 IncludeNetworks []string 1091 ExcludeNetworks []string 1092 Volumes []volumeInfo 1093 } 1094 1095 func (environ *maasEnviron) selectNode(args selectNodeArgs) (*gomaasapi.MAASObject, error) { 1096 var err error 1097 var node gomaasapi.MAASObject 1098 1099 for i, zoneName := range args.AvailabilityZones { 1100 node, err = environ.acquireNode( 1101 args.NodeName, 1102 zoneName, 1103 args.Constraints, 1104 args.IncludeNetworks, 1105 args.ExcludeNetworks, 1106 args.Volumes, 1107 ) 1108 1109 if err, ok := err.(gomaasapi.ServerError); ok && err.StatusCode == http.StatusConflict { 1110 if i+1 < len(args.AvailabilityZones) { 1111 logger.Infof("could not acquire a node in zone %q, trying another zone", zoneName) 1112 continue 1113 } 1114 } 1115 if err != nil { 1116 return nil, errors.Errorf("cannot run instances: %v", err) 1117 } 1118 // Since a return at the end of the function is required 1119 // just break here. 1120 break 1121 } 1122 return &node, nil 1123 } 1124 1125 const bridgeConfigTemplate = ` 1126 # In case we already created the bridge, don't do it again. 1127 grep -q "iface {{.Bridge}} inet dhcp" && exit 0 1128 1129 # Discover primary interface at run-time using the default route (if set) 1130 PRIMARY_IFACE=$(ip route list exact 0/0 | egrep -o 'dev [^ ]+' | cut -b5-) 1131 1132 # If $PRIMARY_IFACE is empty, there's nothing to do. 1133 [ -z "$PRIMARY_IFACE" ] && exit 0 1134 1135 # Change the config to make $PRIMARY_IFACE manual instead of DHCP, 1136 # then create the bridge and enslave $PRIMARY_IFACE into it. 1137 grep -q "iface ${PRIMARY_IFACE} inet dhcp" {{.Config}} && \ 1138 sed -i "s/iface ${PRIMARY_IFACE} inet dhcp//" {{.Config}} && \ 1139 cat >> {{.Config}} << EOF 1140 1141 # Primary interface (defining the default route) 1142 iface ${PRIMARY_IFACE} inet manual 1143 1144 # Bridge to use for LXC/KVM containers 1145 auto {{.Bridge}} 1146 iface {{.Bridge}} inet dhcp 1147 bridge_ports ${PRIMARY_IFACE} 1148 EOF 1149 1150 # Make the primary interface not auto-starting. 1151 grep -q "auto ${PRIMARY_IFACE}" {{.Config}} && \ 1152 sed -i "s/auto ${PRIMARY_IFACE}//" {{.Config}} 1153 1154 # Stop $PRIMARY_IFACE and start the bridge instead. 1155 ifdown -v ${PRIMARY_IFACE} ; ifup -v {{.Bridge}} 1156 1157 # Finally, remove the route using $PRIMARY_IFACE (if any) so it won't 1158 # clash with the same automatically added route for juju-br0 (except 1159 # for the device name). 1160 ip route flush dev $PRIMARY_IFACE scope link proto kernel || true 1161 ` 1162 1163 // setupJujuNetworking returns a string representing the script to run 1164 // in order to prepare the Juju-specific networking config on a node. 1165 func setupJujuNetworking() (string, error) { 1166 parsedTemplate := template.Must( 1167 template.New("BridgeConfig").Parse(bridgeConfigTemplate), 1168 ) 1169 var buf bytes.Buffer 1170 err := parsedTemplate.Execute(&buf, map[string]interface{}{ 1171 "Config": "/etc/network/interfaces", 1172 "Bridge": instancecfg.DefaultBridgeName, 1173 }) 1174 if err != nil { 1175 return "", errors.Annotate(err, "bridge config template error") 1176 } 1177 return buf.String(), nil 1178 } 1179 1180 // newCloudinitConfig creates a cloudinit.Config structure 1181 // suitable as a base for initialising a MAAS node. 1182 func (environ *maasEnviron) newCloudinitConfig(hostname, primaryIface, series string) (cloudinit.CloudConfig, error) { 1183 cloudcfg, err := cloudinit.New(series) 1184 if err != nil { 1185 return nil, err 1186 } 1187 1188 info := machineInfo{hostname} 1189 runCmd, err := info.cloudinitRunCmd(cloudcfg) 1190 if err != nil { 1191 return nil, errors.Trace(err) 1192 } 1193 1194 operatingSystem, err := version.GetOSFromSeries(series) 1195 if err != nil { 1196 return nil, errors.Trace(err) 1197 } 1198 switch operatingSystem { 1199 case version.Windows: 1200 cloudcfg.AddScripts(runCmd) 1201 case version.Ubuntu: 1202 cloudcfg.SetSystemUpdate(true) 1203 cloudcfg.AddScripts("set -xe", runCmd) 1204 // Only create the default bridge if we're not using static 1205 // address allocation for containers. 1206 if !environs.AddressAllocationEnabled() { 1207 // Address allocated feature flag might be disabled, but 1208 // DisableNetworkManagement can still disable the bridge 1209 // creation. 1210 if on, set := environ.Config().DisableNetworkManagement(); on && set { 1211 logger.Infof( 1212 "network management disabled - not using %q bridge for containers", 1213 instancecfg.DefaultBridgeName, 1214 ) 1215 break 1216 } 1217 bridgeScript, err := setupJujuNetworking() 1218 if err != nil { 1219 return nil, errors.Trace(err) 1220 } 1221 cloudcfg.AddPackage("bridge-utils") 1222 cloudcfg.AddRunCmd(bridgeScript) 1223 } 1224 } 1225 return cloudcfg, nil 1226 } 1227 1228 func (environ *maasEnviron) releaseNodes(nodes gomaasapi.MAASObject, ids url.Values, recurse bool) error { 1229 err := ReleaseNodes(nodes, ids) 1230 if err == nil { 1231 return nil 1232 } 1233 maasErr, ok := err.(gomaasapi.ServerError) 1234 if !ok { 1235 return errors.Annotate(err, "cannot release nodes") 1236 } 1237 1238 // StatusCode 409 means a node couldn't be released due to 1239 // a state conflict. Likely it's already released or disk 1240 // erasing. We're assuming an error of 409 *only* means it's 1241 // safe to assume the instance is already released. 1242 // MaaS also releases (or attempts) all nodes, and raises 1243 // a single error on failure. So even with an error 409, all 1244 // nodes have been released. 1245 if maasErr.StatusCode == 409 { 1246 logger.Infof("ignoring error while releasing nodes (%v); all nodes released OK", err) 1247 return nil 1248 } 1249 1250 // a status code of 400, 403 or 404 means one of the nodes 1251 // couldn't be found and none have been released. We have 1252 // to release all the ones we can individually. 1253 if maasErr.StatusCode != 400 && maasErr.StatusCode != 403 && maasErr.StatusCode != 404 { 1254 return errors.Annotate(err, "cannot release nodes") 1255 } 1256 if !recurse { 1257 // this node has already been released and we're golden 1258 return nil 1259 } 1260 1261 var lastErr error 1262 for _, id := range ids["nodes"] { 1263 idFilter := url.Values{} 1264 idFilter.Add("nodes", id) 1265 err := environ.releaseNodes(nodes, idFilter, false) 1266 if err != nil { 1267 lastErr = err 1268 logger.Errorf("error while releasing node %v (%v)", id, err) 1269 } 1270 } 1271 return errors.Trace(lastErr) 1272 1273 } 1274 1275 // StopInstances is specified in the InstanceBroker interface. 1276 func (environ *maasEnviron) StopInstances(ids ...instance.Id) error { 1277 // Shortcut to exit quickly if 'instances' is an empty slice or nil. 1278 if len(ids) == 0 { 1279 return nil 1280 } 1281 nodes := environ.getMAASClient().GetSubObject("nodes") 1282 err := environ.releaseNodes(nodes, getSystemIdValues("nodes", ids), true) 1283 if err != nil { 1284 // error will already have been wrapped 1285 return err 1286 } 1287 return common.RemoveStateInstances(environ.Storage(), ids...) 1288 1289 } 1290 1291 // acquireInstances calls the MAAS API to list acquired nodes. 1292 // 1293 // The "ids" slice is a filter for specific instance IDs. 1294 // Due to how this works in the HTTP API, an empty "ids" 1295 // matches all instances (not none as you might expect). 1296 func (environ *maasEnviron) acquiredInstances(ids []instance.Id) ([]instance.Instance, error) { 1297 filter := getSystemIdValues("id", ids) 1298 filter.Add("agent_name", environ.ecfg().maasAgentName()) 1299 return environ.instances(filter) 1300 } 1301 1302 // instances calls the MAAS API to list nodes matching the given filter. 1303 func (environ *maasEnviron) instances(filter url.Values) ([]instance.Instance, error) { 1304 nodeListing := environ.getMAASClient().GetSubObject("nodes") 1305 listNodeObjects, err := nodeListing.CallGet("list", filter) 1306 if err != nil { 1307 return nil, err 1308 } 1309 listNodes, err := listNodeObjects.GetArray() 1310 if err != nil { 1311 return nil, err 1312 } 1313 instances := make([]instance.Instance, len(listNodes)) 1314 for index, nodeObj := range listNodes { 1315 node, err := nodeObj.GetMAASObject() 1316 if err != nil { 1317 return nil, err 1318 } 1319 instances[index] = &maasInstance{ 1320 maasObject: &node, 1321 environ: environ, 1322 } 1323 } 1324 return instances, nil 1325 } 1326 1327 // Instances returns the instance.Instance objects corresponding to the given 1328 // slice of instance.Id. The error is ErrNoInstances if no instances 1329 // were found. 1330 func (environ *maasEnviron) Instances(ids []instance.Id) ([]instance.Instance, error) { 1331 if len(ids) == 0 { 1332 // This would be treated as "return all instances" below, so 1333 // treat it as a special case. 1334 // The interface requires us to return this particular error 1335 // if no instances were found. 1336 return nil, environs.ErrNoInstances 1337 } 1338 instances, err := environ.acquiredInstances(ids) 1339 if err != nil { 1340 return nil, err 1341 } 1342 if len(instances) == 0 { 1343 return nil, environs.ErrNoInstances 1344 } 1345 1346 idMap := make(map[instance.Id]instance.Instance) 1347 for _, instance := range instances { 1348 idMap[instance.Id()] = instance 1349 } 1350 1351 result := make([]instance.Instance, len(ids)) 1352 for index, id := range ids { 1353 result[index] = idMap[id] 1354 } 1355 1356 if len(instances) < len(ids) { 1357 return result, environs.ErrPartialInstances 1358 } 1359 return result, nil 1360 } 1361 1362 // newDevice creates a new MAAS device for a MAC address, returning the Id of 1363 // the new device. 1364 func (environ *maasEnviron) newDevice(macAddress string, instId instance.Id, hostname string) (string, error) { 1365 client := environ.getMAASClient() 1366 devices := client.GetSubObject("devices") 1367 params := url.Values{} 1368 params.Add("mac_addresses", macAddress) 1369 params.Add("hostname", hostname) 1370 params.Add("parent", extractSystemId(instId)) 1371 logger.Tracef("creating a new MAAS device for MAC %q, hostname %q, parent %q", macAddress, hostname, string(instId)) 1372 result, err := devices.CallPost("new", params) 1373 if err != nil { 1374 return "", errors.Trace(err) 1375 } 1376 1377 resultMap, err := result.GetMap() 1378 if err != nil { 1379 return "", errors.Trace(err) 1380 } 1381 1382 device, err := resultMap["system_id"].GetString() 1383 if err != nil { 1384 return "", errors.Trace(err) 1385 } 1386 return device, nil 1387 } 1388 1389 // fetchFullDevice fetches an existing device Id associated with a MAC address, or 1390 // returns an error if there is no device. 1391 func (environ *maasEnviron) fetchFullDevice(macAddress string) (map[string]gomaasapi.JSONObject, error) { 1392 client := environ.getMAASClient() 1393 devices := client.GetSubObject("devices") 1394 params := url.Values{} 1395 params.Add("mac_address", macAddress) 1396 result, err := devices.CallGet("list", params) 1397 if err != nil { 1398 return nil, errors.Trace(err) 1399 } 1400 resultArray, err := result.GetArray() 1401 if err != nil { 1402 return nil, errors.Trace(err) 1403 } 1404 if len(resultArray) == 0 { 1405 return nil, errors.NotFoundf("no device for MAC %q", macAddress) 1406 } 1407 if len(resultArray) != 1 { 1408 return nil, errors.Errorf("unexpected response, expected 1 device got %d", len(resultArray)) 1409 } 1410 resultMap, err := resultArray[0].GetMap() 1411 if err != nil { 1412 return nil, errors.Trace(err) 1413 } 1414 return resultMap, nil 1415 } 1416 1417 func (environ *maasEnviron) fetchDevice(macAddress string) (string, error) { 1418 deviceMap, err := environ.fetchFullDevice(macAddress) 1419 if err != nil { 1420 return "", errors.Trace(err) 1421 } 1422 1423 deviceId, err := deviceMap["system_id"].GetString() 1424 if err != nil { 1425 return "", errors.Trace(err) 1426 } 1427 return deviceId, nil 1428 } 1429 1430 // createOrFetchDevice returns a device Id associated with a MAC address. If 1431 // there is not already one it will create one. 1432 func (environ *maasEnviron) createOrFetchDevice(macAddress string, instId instance.Id, hostname string) (string, error) { 1433 device, err := environ.fetchDevice(macAddress) 1434 if err == nil { 1435 return device, nil 1436 } 1437 if !errors.IsNotFound(err) { 1438 return "", errors.Trace(err) 1439 } 1440 device, err = environ.newDevice(macAddress, instId, hostname) 1441 if err != nil { 1442 return "", errors.Trace(err) 1443 } 1444 return device, nil 1445 } 1446 1447 // AllocateAddress requests an address to be allocated for the 1448 // given instance on the given network. 1449 func (environ *maasEnviron) AllocateAddress(instId instance.Id, subnetId network.Id, addr network.Address, macAddress, hostname string) (err error) { 1450 if !environs.AddressAllocationEnabled() { 1451 return errors.NotSupportedf("address allocation") 1452 } 1453 defer errors.DeferredAnnotatef(&err, "failed to allocate address %q for instance %q", addr, instId) 1454 1455 client := environ.getMAASClient() 1456 var maasErr gomaasapi.ServerError 1457 supportsDevices, err := environ.supportsDevices() 1458 if err != nil { 1459 return err 1460 } 1461 if supportsDevices { 1462 device, err := environ.createOrFetchDevice(macAddress, instId, hostname) 1463 if err != nil { 1464 return err 1465 } 1466 1467 devices := client.GetSubObject("devices") 1468 err = ReserveIPAddressOnDevice(devices, device, addr) 1469 if err == nil { 1470 logger.Infof("allocated address %q for instance %q on device %q", addr, instId, device) 1471 return nil 1472 } 1473 1474 var ok bool 1475 maasErr, ok = err.(gomaasapi.ServerError) 1476 if !ok { 1477 return errors.Trace(err) 1478 } 1479 } else { 1480 1481 var subnets []network.SubnetInfo 1482 1483 subnets, err = environ.Subnets(instId, []network.Id{subnetId}) 1484 logger.Tracef("Subnets(%q, %q, %q) returned: %v (%v)", instId, subnetId, addr, subnets, err) 1485 if err != nil { 1486 return errors.Trace(err) 1487 } 1488 if len(subnets) != 1 { 1489 return errors.Errorf("could not find subnet matching %q", subnetId) 1490 } 1491 foundSub := subnets[0] 1492 logger.Tracef("found subnet %#v", foundSub) 1493 1494 cidr := foundSub.CIDR 1495 ipaddresses := client.GetSubObject("ipaddresses") 1496 err = ReserveIPAddress(ipaddresses, cidr, addr) 1497 if err == nil { 1498 logger.Infof("allocated address %q for instance %q on subnet %q", addr, instId, cidr) 1499 return nil 1500 } 1501 1502 var ok bool 1503 maasErr, ok = err.(gomaasapi.ServerError) 1504 if !ok { 1505 return errors.Trace(err) 1506 } 1507 } 1508 // For an "out of range" IP address, maas raises 1509 // StaticIPAddressOutOfRange - an error 403 1510 // If there are no more addresses we get 1511 // StaticIPAddressExhaustion - an error 503 1512 // For an address already in use we get 1513 // StaticIPAddressUnavailable - an error 404 1514 if maasErr.StatusCode == 404 { 1515 logger.Tracef("address %q not available for allocation", addr) 1516 return environs.ErrIPAddressUnavailable 1517 } else if maasErr.StatusCode == 503 { 1518 logger.Tracef("no more addresses available on the subnet") 1519 return environs.ErrIPAddressesExhausted 1520 } 1521 // any error other than a 404 or 503 is "unexpected" and should 1522 // be returned directly. 1523 return errors.Trace(err) 1524 } 1525 1526 // ReleaseAddress releases a specific address previously allocated with 1527 // AllocateAddress. 1528 func (environ *maasEnviron) ReleaseAddress(instId instance.Id, _ network.Id, addr network.Address, macAddress string) (err error) { 1529 if !environs.AddressAllocationEnabled() { 1530 return errors.NotSupportedf("address allocation") 1531 } 1532 1533 defer errors.DeferredAnnotatef(&err, "failed to release IP address %q from instance %q", addr, instId) 1534 1535 supportsDevices, err := environ.supportsDevices() 1536 if err != nil { 1537 return err 1538 } 1539 1540 logger.Infof("releasing address: %q, MAC address: %q, supports devices: %v", addr, macAddress, supportsDevices) 1541 // Addresses originally allocated without a device will have macAddress 1542 // set to "". We shouldn't look for a device for these addresses. 1543 if supportsDevices && macAddress != "" { 1544 device, err := environ.fetchFullDevice(macAddress) 1545 if err == nil { 1546 addresses, err := device["ip_addresses"].GetArray() 1547 if err != nil { 1548 return err 1549 } 1550 systemId, err := device["system_id"].GetString() 1551 if err != nil { 1552 return err 1553 } 1554 1555 if len(addresses) == 1 { 1556 // With our current usage of devices they will always 1557 // have exactly one IP address, but in theory that 1558 // could change and this code will continue to work. 1559 // The device is only destroyed when we come to release 1560 // the last address. Race conditions aside. 1561 deviceAPI := environ.getMAASClient().GetSubObject("devices").GetSubObject(systemId) 1562 err = deviceAPI.Delete() 1563 return err 1564 } 1565 } else if !errors.IsNotFound(err) { 1566 return err 1567 } 1568 // No device for this IP address, release the address normally. 1569 } 1570 1571 ipaddresses := environ.getMAASClient().GetSubObject("ipaddresses") 1572 retries := 0 1573 for a := shortAttempt.Start(); a.Next(); { 1574 retries++ 1575 // This can return a 404 error if the address has already been released 1576 // or is unknown by maas. However this, like any other error, would be 1577 // unexpected - so we don't treat it specially and just return it to 1578 // the caller. 1579 err = ReleaseIPAddress(ipaddresses, addr) 1580 if err == nil { 1581 break 1582 } 1583 logger.Infof("failed to release address %q from instance %q, will retry", addr, instId) 1584 } 1585 if err != nil { 1586 logger.Warningf("failed to release address %q from instance %q after %d attempts: %v", addr, instId, retries, err) 1587 } 1588 return err 1589 } 1590 1591 // NetworkInterfaces implements Environ.NetworkInterfaces. 1592 func (environ *maasEnviron) NetworkInterfaces(instId instance.Id) ([]network.InterfaceInfo, error) { 1593 instances, err := environ.acquiredInstances([]instance.Id{instId}) 1594 if err != nil { 1595 return nil, errors.Annotatef(err, "could not find instance %q", instId) 1596 } 1597 if len(instances) == 0 { 1598 return nil, errors.NotFoundf("instance %q", instId) 1599 } 1600 inst := instances[0] 1601 interfaces, _, err := environ.getInstanceNetworkInterfaces(inst) 1602 if err != nil { 1603 return nil, errors.Annotatef(err, "failed to get instance %q network interfaces", instId) 1604 } 1605 1606 networks, err := environ.getInstanceNetworks(inst) 1607 if err != nil { 1608 return nil, errors.Annotatef(err, "failed to get instance %q subnets", instId) 1609 } 1610 1611 macToNetworkMap := make(map[string]networkDetails) 1612 for _, network := range networks { 1613 macs, err := environ.listConnectedMacs(network) 1614 if err != nil { 1615 return nil, errors.Trace(err) 1616 } 1617 for _, mac := range macs { 1618 macToNetworkMap[mac] = network 1619 } 1620 } 1621 1622 result := []network.InterfaceInfo{} 1623 for serial, iface := range interfaces { 1624 deviceIndex := iface.DeviceIndex 1625 interfaceName := iface.InterfaceName 1626 disabled := iface.Disabled 1627 1628 ifaceInfo := network.InterfaceInfo{ 1629 DeviceIndex: deviceIndex, 1630 InterfaceName: interfaceName, 1631 Disabled: disabled, 1632 NoAutoStart: disabled, 1633 MACAddress: serial, 1634 ConfigType: network.ConfigDHCP, 1635 } 1636 details, ok := macToNetworkMap[serial] 1637 if ok { 1638 ifaceInfo.VLANTag = details.VLANTag 1639 ifaceInfo.ProviderSubnetId = network.Id(details.Name) 1640 mask := net.IPMask(net.ParseIP(details.Mask)) 1641 cidr := net.IPNet{net.ParseIP(details.IP), mask} 1642 ifaceInfo.CIDR = cidr.String() 1643 ifaceInfo.Address = network.NewAddress(cidr.IP.String()) 1644 } else { 1645 logger.Debugf("no subnet information for MAC address %q, instance %q", serial, instId) 1646 } 1647 result = append(result, ifaceInfo) 1648 } 1649 return result, nil 1650 } 1651 1652 // listConnectedMacs calls the MAAS list_connected_macs API to fetch all the 1653 // the MAC addresses attached to a specific network. 1654 func (environ *maasEnviron) listConnectedMacs(network networkDetails) ([]string, error) { 1655 client := environ.getMAASClient().GetSubObject("networks").GetSubObject(network.Name) 1656 json, err := client.CallGet("list_connected_macs", nil) 1657 if err != nil { 1658 return nil, err 1659 } 1660 1661 macs, err := json.GetArray() 1662 if err != nil { 1663 return nil, err 1664 } 1665 result := []string{} 1666 for _, macObj := range macs { 1667 macMap, err := macObj.GetMap() 1668 if err != nil { 1669 return nil, err 1670 } 1671 mac, err := macMap["mac_address"].GetString() 1672 if err != nil { 1673 return nil, err 1674 } 1675 1676 result = append(result, mac) 1677 } 1678 return result, nil 1679 } 1680 1681 // Subnets returns basic information about the specified subnets known 1682 // by the provider for the specified instance. subnetIds must not be 1683 // empty. Implements NetworkingEnviron.Subnets. 1684 func (environ *maasEnviron) Subnets(instId instance.Id, subnetIds []network.Id) ([]network.SubnetInfo, error) { 1685 // At some point in the future an empty netIds may mean "fetch all subnets" 1686 // but until that functionality is needed it's an error. 1687 if len(subnetIds) == 0 { 1688 return nil, errors.Errorf("subnetIds must not be empty") 1689 } 1690 instances, err := environ.acquiredInstances([]instance.Id{instId}) 1691 if err != nil { 1692 return nil, errors.Annotatef(err, "could not find instance %q", instId) 1693 } 1694 if len(instances) == 0 { 1695 return nil, errors.NotFoundf("instance %v", instId) 1696 } 1697 inst := instances[0] 1698 // The MAAS API get networks call returns named subnets, not physical networks, 1699 // so we save the data from this call into a variable called subnets. 1700 // http://maas.ubuntu.com/docs/api.html#networks 1701 subnets, err := environ.getInstanceNetworks(inst) 1702 if err != nil { 1703 return nil, errors.Annotatef(err, "cannot get instance %q subnets", instId) 1704 } 1705 logger.Debugf("instance %q has subnets %v", instId, subnets) 1706 1707 nodegroups, err := environ.getNodegroups() 1708 if err != nil { 1709 return nil, errors.Annotatef(err, "cannot get instance %q node groups", instId) 1710 } 1711 nodegroupInterfaces := environ.getNodegroupInterfaces(nodegroups) 1712 1713 subnetIdSet := make(map[network.Id]bool) 1714 for _, netId := range subnetIds { 1715 subnetIdSet[netId] = false 1716 } 1717 processedIds := make(map[network.Id]bool) 1718 1719 var networkInfo []network.SubnetInfo 1720 for _, subnet := range subnets { 1721 _, ok := subnetIdSet[network.Id(subnet.Name)] 1722 if !ok { 1723 // This id is not what we're looking for. 1724 continue 1725 } 1726 if _, ok := processedIds[network.Id(subnet.Name)]; ok { 1727 // Don't add the same subnet twice. 1728 continue 1729 } 1730 // mark that we've found this subnet 1731 processedIds[network.Id(subnet.Name)] = true 1732 subnetIdSet[network.Id(subnet.Name)] = true 1733 netCIDR := &net.IPNet{ 1734 IP: net.ParseIP(subnet.IP), 1735 Mask: net.IPMask(net.ParseIP(subnet.Mask)), 1736 } 1737 var allocatableHigh, allocatableLow net.IP 1738 for ip, bounds := range nodegroupInterfaces { 1739 contained := netCIDR.Contains(net.ParseIP(ip)) 1740 if contained { 1741 allocatableLow = bounds[0] 1742 allocatableHigh = bounds[1] 1743 break 1744 } 1745 } 1746 subnetInfo := network.SubnetInfo{ 1747 CIDR: netCIDR.String(), 1748 VLANTag: subnet.VLANTag, 1749 ProviderId: network.Id(subnet.Name), 1750 AllocatableIPLow: allocatableLow, 1751 AllocatableIPHigh: allocatableHigh, 1752 } 1753 1754 // Verify we filled-in everything for all networks 1755 // and drop incomplete records. 1756 if subnetInfo.ProviderId == "" || subnetInfo.CIDR == "" { 1757 logger.Infof("ignoring subnet %q: missing information (%#v)", subnet.Name, subnetInfo) 1758 continue 1759 } 1760 1761 logger.Tracef("found subnet with info %#v", subnetInfo) 1762 networkInfo = append(networkInfo, subnetInfo) 1763 } 1764 logger.Debugf("available subnets for instance %v: %#v", inst.Id(), networkInfo) 1765 1766 notFound := []network.Id{} 1767 for subnetId, found := range subnetIdSet { 1768 if !found { 1769 notFound = append(notFound, subnetId) 1770 } 1771 } 1772 if len(notFound) != 0 { 1773 return nil, errors.Errorf("failed to find the following subnets: %v", notFound) 1774 } 1775 1776 return networkInfo, nil 1777 } 1778 1779 // AllInstances returns all the instance.Instance in this provider. 1780 func (environ *maasEnviron) AllInstances() ([]instance.Instance, error) { 1781 return environ.acquiredInstances(nil) 1782 } 1783 1784 // Storage is defined by the Environ interface. 1785 func (env *maasEnviron) Storage() storage.Storage { 1786 env.ecfgMutex.Lock() 1787 defer env.ecfgMutex.Unlock() 1788 return env.storageUnlocked 1789 } 1790 1791 func (environ *maasEnviron) Destroy() error { 1792 if err := common.Destroy(environ); err != nil { 1793 return errors.Trace(err) 1794 } 1795 return environ.Storage().RemoveAll() 1796 } 1797 1798 // MAAS does not do firewalling so these port methods do nothing. 1799 func (*maasEnviron) OpenPorts([]network.PortRange) error { 1800 logger.Debugf("unimplemented OpenPorts() called") 1801 return nil 1802 } 1803 1804 func (*maasEnviron) ClosePorts([]network.PortRange) error { 1805 logger.Debugf("unimplemented ClosePorts() called") 1806 return nil 1807 } 1808 1809 func (*maasEnviron) Ports() ([]network.PortRange, error) { 1810 logger.Debugf("unimplemented Ports() called") 1811 return nil, nil 1812 } 1813 1814 func (*maasEnviron) Provider() environs.EnvironProvider { 1815 return &providerInstance 1816 } 1817 1818 // networkDetails holds information about a MAAS network. 1819 type networkDetails struct { 1820 Name string 1821 IP string 1822 Mask string 1823 VLANTag int 1824 Description string 1825 } 1826 1827 // getInstanceNetworks returns a list of all MAAS networks for a given node. 1828 func (environ *maasEnviron) getInstanceNetworks(inst instance.Instance) ([]networkDetails, error) { 1829 maasInst := inst.(*maasInstance) 1830 maasObj := maasInst.maasObject 1831 client := environ.getMAASClient().GetSubObject("networks") 1832 nodeId, err := maasObj.GetField("system_id") 1833 if err != nil { 1834 return nil, err 1835 } 1836 params := url.Values{"node": {nodeId}} 1837 json, err := client.CallGet("", params) 1838 if err != nil { 1839 return nil, err 1840 } 1841 jsonNets, err := json.GetArray() 1842 if err != nil { 1843 return nil, err 1844 } 1845 1846 networks := make([]networkDetails, len(jsonNets)) 1847 for i, jsonNet := range jsonNets { 1848 fields, err := jsonNet.GetMap() 1849 if err != nil { 1850 return nil, err 1851 } 1852 name, err := fields["name"].GetString() 1853 if err != nil { 1854 return nil, fmt.Errorf("cannot get name: %v", err) 1855 } 1856 ip, err := fields["ip"].GetString() 1857 if err != nil { 1858 return nil, fmt.Errorf("cannot get ip: %v", err) 1859 } 1860 netmask, err := fields["netmask"].GetString() 1861 if err != nil { 1862 return nil, fmt.Errorf("cannot get netmask: %v", err) 1863 } 1864 vlanTag := 0 1865 vlanTagField, ok := fields["vlan_tag"] 1866 if ok && !vlanTagField.IsNil() { 1867 // vlan_tag is optional, so assume it's 0 when missing or nil. 1868 vlanTagFloat, err := vlanTagField.GetFloat64() 1869 if err != nil { 1870 return nil, fmt.Errorf("cannot get vlan_tag: %v", err) 1871 } 1872 vlanTag = int(vlanTagFloat) 1873 } 1874 description, err := fields["description"].GetString() 1875 if err != nil { 1876 return nil, fmt.Errorf("cannot get description: %v", err) 1877 } 1878 1879 networks[i] = networkDetails{ 1880 Name: name, 1881 IP: ip, 1882 Mask: netmask, 1883 VLANTag: vlanTag, 1884 Description: description, 1885 } 1886 } 1887 return networks, nil 1888 } 1889 1890 // getNetworkMACs returns all MAC addresses connected to the given 1891 // network. 1892 func (environ *maasEnviron) getNetworkMACs(networkName string) ([]string, error) { 1893 client := environ.getMAASClient().GetSubObject("networks").GetSubObject(networkName) 1894 json, err := client.CallGet("list_connected_macs", nil) 1895 if err != nil { 1896 return nil, err 1897 } 1898 jsonMACs, err := json.GetArray() 1899 if err != nil { 1900 return nil, err 1901 } 1902 1903 macs := make([]string, len(jsonMACs)) 1904 for i, jsonMAC := range jsonMACs { 1905 fields, err := jsonMAC.GetMap() 1906 if err != nil { 1907 return nil, err 1908 } 1909 macAddress, err := fields["mac_address"].GetString() 1910 if err != nil { 1911 return nil, fmt.Errorf("cannot get mac_address: %v", err) 1912 } 1913 macs[i] = macAddress 1914 } 1915 return macs, nil 1916 } 1917 1918 // getInstanceNetworkInterfaces returns a map of interface MAC address 1919 // to ifaceInfo for each network interface of the given instance, as 1920 // discovered during the commissioning phase. In addition, it also 1921 // returns the interface name discovered as primary. 1922 func (environ *maasEnviron) getInstanceNetworkInterfaces(inst instance.Instance) (map[string]ifaceInfo, string, error) { 1923 maasInst := inst.(*maasInstance) 1924 maasObj := maasInst.maasObject 1925 result, err := maasObj.CallGet("details", nil) 1926 if err != nil { 1927 return nil, "", errors.Trace(err) 1928 } 1929 // Get the node's lldp / lshw details discovered at commissioning. 1930 data, err := result.GetBytes() 1931 if err != nil { 1932 return nil, "", errors.Trace(err) 1933 } 1934 var parsed map[string]interface{} 1935 if err := bson.Unmarshal(data, &parsed); err != nil { 1936 return nil, "", errors.Trace(err) 1937 } 1938 lshwData, ok := parsed["lshw"] 1939 if !ok { 1940 return nil, "", errors.Errorf("no hardware information available for node %q", inst.Id()) 1941 } 1942 lshwXML, ok := lshwData.([]byte) 1943 if !ok { 1944 return nil, "", errors.Errorf("invalid hardware information for node %q", inst.Id()) 1945 } 1946 // Now we have the lshw XML data, parse it to extract and return NICs. 1947 return extractInterfaces(inst, lshwXML) 1948 } 1949 1950 type ifaceInfo struct { 1951 DeviceIndex int 1952 InterfaceName string 1953 Disabled bool 1954 } 1955 1956 // extractInterfaces parses the XML output of lswh and extracts all 1957 // network interfaces, returing a map MAC address to ifaceInfo, as 1958 // well as the interface name discovered as primary. 1959 func extractInterfaces(inst instance.Instance, lshwXML []byte) (map[string]ifaceInfo, string, error) { 1960 type Node struct { 1961 Id string `xml:"id,attr"` 1962 Disabled bool `xml:"disabled,attr,omitempty"` 1963 Description string `xml:"description"` 1964 Serial string `xml:"serial"` 1965 LogicalName string `xml:"logicalname"` 1966 Children []Node `xml:"node"` 1967 } 1968 type List struct { 1969 Nodes []Node `xml:"node"` 1970 } 1971 var lshw List 1972 if err := xml.Unmarshal(lshwXML, &lshw); err != nil { 1973 return nil, "", errors.Annotatef(err, "cannot parse lshw XML details for node %q", inst.Id()) 1974 } 1975 primaryIface := "" 1976 interfaces := make(map[string]ifaceInfo) 1977 var processNodes func(nodes []Node) error 1978 var baseIndex int 1979 processNodes = func(nodes []Node) error { 1980 for _, node := range nodes { 1981 if strings.HasPrefix(node.Id, "network") { 1982 index := baseIndex 1983 if strings.HasPrefix(node.Id, "network:") { 1984 // There is an index suffix, parse it. 1985 var err error 1986 index, err = strconv.Atoi(strings.TrimPrefix(node.Id, "network:")) 1987 if err != nil { 1988 return errors.Annotatef(err, "lshw output for node %q has invalid ID suffix for %q", inst.Id(), node.Id) 1989 } 1990 } else { 1991 baseIndex++ 1992 } 1993 1994 if primaryIface == "" && !node.Disabled { 1995 primaryIface = node.LogicalName 1996 logger.Debugf("node %q primary network interface is %q", inst.Id(), primaryIface) 1997 } 1998 interfaces[node.Serial] = ifaceInfo{ 1999 DeviceIndex: index, 2000 InterfaceName: node.LogicalName, 2001 Disabled: node.Disabled, 2002 } 2003 if node.Disabled { 2004 logger.Debugf("node %q skipping disabled network interface %q", inst.Id(), node.LogicalName) 2005 } 2006 2007 } 2008 if err := processNodes(node.Children); err != nil { 2009 return err 2010 } 2011 } 2012 return nil 2013 } 2014 err := processNodes(lshw.Nodes) 2015 return interfaces, primaryIface, err 2016 }