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