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