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