github.com/mwhudson/juju@v0.0.0-20160512215208-90ff01f3497f/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 "encoding/json" 8 "fmt" 9 "net" 10 "net/http" 11 "net/url" 12 "regexp" 13 "strconv" 14 "strings" 15 "sync" 16 "time" 17 18 "github.com/juju/errors" 19 "github.com/juju/gomaasapi" 20 "github.com/juju/names" 21 "github.com/juju/utils" 22 "github.com/juju/utils/featureflag" 23 "github.com/juju/utils/os" 24 "github.com/juju/utils/series" 25 "github.com/juju/utils/set" 26 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/feature" 35 "github.com/juju/juju/instance" 36 "github.com/juju/juju/network" 37 "github.com/juju/juju/provider/common" 38 "github.com/juju/juju/state/multiwatcher" 39 "github.com/juju/juju/status" 40 "github.com/juju/juju/tools" 41 ) 42 43 const ( 44 // The string from the api indicating the dynamic range of a subnet. 45 dynamicRange = "dynamic-range" 46 // The version strings indicating the MAAS API version. 47 apiVersion1 = "1.0" 48 apiVersion2 = "2.0" 49 ) 50 51 // A request may fail to due "eventual consistency" semantics, which 52 // should resolve fairly quickly. A request may also fail due to a slow 53 // state transition (for instance an instance taking a while to release 54 // a security group after termination). The former failure mode is 55 // dealt with by shortAttempt, the latter by LongAttempt. 56 var shortAttempt = utils.AttemptStrategy{ 57 Total: 5 * time.Second, 58 Delay: 200 * time.Millisecond, 59 } 60 61 var ( 62 ReleaseNodes = releaseNodes 63 DeploymentStatusCall = deploymentStatusCall 64 GetCapabilities = getCapabilities 65 GetMAAS2Controller = getMAAS2Controller 66 ) 67 68 func getMAAS2Controller(maasServer, apiKey string) (gomaasapi.Controller, error) { 69 return gomaasapi.NewController(gomaasapi.ControllerArgs{ 70 BaseURL: maasServer, 71 APIKey: apiKey, 72 }) 73 } 74 75 func subnetToSpaceIds(spaces gomaasapi.MAASObject) (map[string]network.Id, error) { 76 spacesJson, err := spaces.CallGet("", nil) 77 if err != nil { 78 return nil, errors.Trace(err) 79 } 80 spacesArray, err := spacesJson.GetArray() 81 if err != nil { 82 return nil, errors.Trace(err) 83 } 84 subnetsMap := make(map[string]network.Id) 85 for _, spaceJson := range spacesArray { 86 spaceMap, err := spaceJson.GetMap() 87 if err != nil { 88 return nil, errors.Trace(err) 89 } 90 providerIdRaw, err := spaceMap["id"].GetFloat64() 91 if err != nil { 92 return nil, errors.Trace(err) 93 } 94 providerId := network.Id(fmt.Sprintf("%.0f", providerIdRaw)) 95 subnetsArray, err := spaceMap["subnets"].GetArray() 96 if err != nil { 97 return nil, errors.Trace(err) 98 } 99 for _, subnetJson := range subnetsArray { 100 subnetMap, err := subnetJson.GetMap() 101 if err != nil { 102 return nil, errors.Trace(err) 103 } 104 subnet, err := subnetMap["cidr"].GetString() 105 if err != nil { 106 return nil, errors.Trace(err) 107 } 108 subnetsMap[subnet] = providerId 109 } 110 } 111 return subnetsMap, nil 112 } 113 114 func releaseNodes(nodes gomaasapi.MAASObject, ids url.Values) error { 115 _, err := nodes.CallPost("release", ids) 116 return err 117 } 118 119 type maasEnviron struct { 120 common.SupportsUnitPlacementPolicy 121 122 name string 123 124 // archMutex gates access to supportedArchitectures 125 archMutex sync.Mutex 126 // supportedArchitectures caches the architectures 127 // for which images can be instantiated. 128 supportedArchitectures []string 129 130 // ecfgMutex protects the *Unlocked fields below. 131 ecfgMutex sync.Mutex 132 133 ecfgUnlocked *maasModelConfig 134 maasClientUnlocked *gomaasapi.MAASObject 135 storageUnlocked storage.Storage 136 137 // maasController provides access to the MAAS 2.0 API. 138 maasController gomaasapi.Controller 139 140 availabilityZonesMutex sync.Mutex 141 availabilityZones []common.AvailabilityZone 142 143 // apiVersion tells us if we are using the MAAS 1.0 or 2.0 api. 144 apiVersion string 145 } 146 147 var _ environs.Environ = (*maasEnviron)(nil) 148 149 func NewEnviron(cfg *config.Config) (*maasEnviron, error) { 150 env := new(maasEnviron) 151 err := env.SetConfig(cfg) 152 if err != nil { 153 return nil, err 154 } 155 env.name = cfg.Name() 156 env.storageUnlocked = NewStorage(env) 157 158 return env, nil 159 } 160 161 func (env *maasEnviron) usingMAAS2() bool { 162 return env.apiVersion == apiVersion2 163 } 164 165 // Bootstrap is specified in the Environ interface. 166 func (env *maasEnviron) Bootstrap(ctx environs.BootstrapContext, args environs.BootstrapParams) (*environs.BootstrapResult, error) { 167 if featureflag.Enabled(feature.AddressAllocation) { 168 logger.Warningf("address-allocation feature flag is no longer supported on MAAS and is ignored!") 169 } 170 171 result, series, finalizer, err := common.BootstrapInstance(ctx, env, args) 172 if err != nil { 173 return nil, err 174 } 175 176 // We want to destroy the started instance if it doesn't transition to Deployed. 177 defer func() { 178 if err != nil { 179 if err := env.StopInstances(result.Instance.Id()); err != nil { 180 logger.Errorf("error releasing bootstrap instance: %v", err) 181 } 182 } 183 }() 184 // Wait for bootstrap instance to change to deployed state. 185 if err := env.waitForNodeDeployment(result.Instance.Id()); err != nil { 186 return nil, errors.Annotate(err, "bootstrap instance started but did not change to Deployed state") 187 } 188 189 bsResult := &environs.BootstrapResult{ 190 Arch: *result.Hardware.Arch, 191 Series: series, 192 Finalize: finalizer, 193 } 194 return bsResult, nil 195 } 196 197 // ControllerInstances is specified in the Environ interface. 198 func (env *maasEnviron) ControllerInstances() ([]instance.Id, error) { 199 return common.ProviderStateInstances(env, env.Storage()) 200 } 201 202 // ecfg returns the environment's maasModelConfig, and protects it with a 203 // mutex. 204 func (env *maasEnviron) ecfg() *maasModelConfig { 205 env.ecfgMutex.Lock() 206 cfg := *env.ecfgUnlocked 207 env.ecfgMutex.Unlock() 208 return &cfg 209 } 210 211 // Config is specified in the Environ interface. 212 func (env *maasEnviron) Config() *config.Config { 213 return env.ecfg().Config 214 } 215 216 // SetConfig is specified in the Environ interface. 217 func (env *maasEnviron) SetConfig(cfg *config.Config) error { 218 env.ecfgMutex.Lock() 219 defer env.ecfgMutex.Unlock() 220 221 // The new config has already been validated by itself, but now we 222 // validate the transition from the old config to the new. 223 var oldCfg *config.Config 224 if env.ecfgUnlocked != nil { 225 oldCfg = env.ecfgUnlocked.Config 226 } 227 cfg, err := env.Provider().Validate(cfg, oldCfg) 228 if err != nil { 229 return errors.Trace(err) 230 } 231 232 ecfg, err := providerInstance.newConfig(cfg) 233 if err != nil { 234 return errors.Trace(err) 235 } 236 237 env.ecfgUnlocked = ecfg 238 239 // We need to know the version of the server we're on. We support 1.9 240 // and 2.0. MAAS 1.9 uses the 1.0 api version and 2.0 uses the 2.0 api 241 // version. 242 apiVersion := apiVersion2 243 controller, err := GetMAAS2Controller(ecfg.maasServer(), ecfg.maasOAuth()) 244 switch { 245 case gomaasapi.IsUnsupportedVersionError(err): 246 apiVersion = apiVersion1 247 authClient, err := gomaasapi.NewAuthenticatedClient(ecfg.maasServer(), ecfg.maasOAuth(), apiVersion1) 248 if err != nil { 249 return errors.Trace(err) 250 } 251 env.maasClientUnlocked = gomaasapi.NewMAAS(*authClient) 252 caps, err := GetCapabilities(env.maasClientUnlocked) 253 if err != nil { 254 return errors.Trace(err) 255 } 256 if !caps.Contains(capNetworkDeploymentUbuntu) { 257 return errors.NotSupportedf("MAAS 1.9 or more recent is required") 258 } 259 case err != nil: 260 return errors.Trace(err) 261 default: 262 env.maasController = controller 263 } 264 env.apiVersion = apiVersion 265 return nil 266 } 267 268 // SupportedArchitectures is specified on the EnvironCapability interface. 269 func (env *maasEnviron) SupportedArchitectures() ([]string, error) { 270 env.archMutex.Lock() 271 defer env.archMutex.Unlock() 272 if env.supportedArchitectures != nil { 273 return env.supportedArchitectures, nil 274 } 275 276 fetchArchitectures := env.allArchitecturesWithFallback 277 if env.usingMAAS2() { 278 fetchArchitectures = env.allArchitectures2 279 } 280 architectures, err := fetchArchitectures() 281 if err != nil { 282 return nil, errors.Trace(err) 283 } 284 env.supportedArchitectures = architectures 285 return env.supportedArchitectures, nil 286 } 287 288 // SupportsSpaces is specified on environs.Networking. 289 func (env *maasEnviron) SupportsSpaces() (bool, error) { 290 return true, nil 291 } 292 293 // SupportsSpaceDiscovery is specified on environs.Networking. 294 func (env *maasEnviron) SupportsSpaceDiscovery() (bool, error) { 295 return true, nil 296 } 297 298 // SupportsAddressAllocation is specified on environs.Networking. 299 func (env *maasEnviron) SupportsAddressAllocation(_ network.Id) (bool, error) { 300 return false, errors.NotSupportedf("legacy address allocation") 301 } 302 303 // allArchitectures2 uses the MAAS2 controller to get architectures from boot 304 // resources. 305 func (env *maasEnviron) allArchitectures2() ([]string, error) { 306 resources, err := env.maasController.BootResources() 307 if err != nil { 308 return nil, errors.Trace(err) 309 } 310 architectures := set.NewStrings() 311 for _, resource := range resources { 312 architectures.Add(strings.Split(resource.Architecture(), "/")[0]) 313 } 314 return architectures.SortedValues(), nil 315 } 316 317 // allArchitectureWithFallback queries MAAS for all of the boot-images 318 // across all registered nodegroups and collapses them down to unique 319 // architectures. 320 func (env *maasEnviron) allArchitecturesWithFallback() ([]string, error) { 321 architectures, err := env.allArchitectures() 322 if err != nil || len(architectures) == 0 { 323 logger.Debugf("error querying boot-images: %v", err) 324 logger.Debugf("falling back to listing nodes") 325 architectures, err := env.nodeArchitectures() 326 if err != nil { 327 return nil, errors.Trace(err) 328 } 329 return architectures, nil 330 } else { 331 return architectures, nil 332 } 333 } 334 335 func (env *maasEnviron) allArchitectures() ([]string, error) { 336 nodegroups, err := env.getNodegroups() 337 if err != nil { 338 return nil, err 339 } 340 architectures := set.NewStrings() 341 for _, nodegroup := range nodegroups { 342 bootImages, err := env.nodegroupBootImages(nodegroup) 343 if err != nil { 344 return nil, errors.Annotatef(err, "cannot get boot images for nodegroup %v", nodegroup) 345 } 346 for _, image := range bootImages { 347 architectures.Add(image.architecture) 348 } 349 } 350 return architectures.SortedValues(), nil 351 } 352 353 // getNodegroups returns the UUID corresponding to each nodegroup 354 // in the MAAS installation. 355 func (env *maasEnviron) getNodegroups() ([]string, error) { 356 nodegroupsListing := env.getMAASClient().GetSubObject("nodegroups") 357 nodegroupsResult, err := nodegroupsListing.CallGet("list", nil) 358 if err != nil { 359 return nil, err 360 } 361 list, err := nodegroupsResult.GetArray() 362 if err != nil { 363 return nil, err 364 } 365 nodegroups := make([]string, len(list)) 366 for i, obj := range list { 367 nodegroup, err := obj.GetMap() 368 if err != nil { 369 return nil, err 370 } 371 uuid, err := nodegroup["uuid"].GetString() 372 if err != nil { 373 return nil, err 374 } 375 nodegroups[i] = uuid 376 } 377 return nodegroups, nil 378 } 379 380 type bootImage struct { 381 architecture string 382 release string 383 } 384 385 // nodegroupBootImages returns the set of boot-images for the specified nodegroup. 386 func (env *maasEnviron) nodegroupBootImages(nodegroupUUID string) ([]bootImage, error) { 387 nodegroupObject := env.getMAASClient().GetSubObject("nodegroups").GetSubObject(nodegroupUUID) 388 bootImagesObject := nodegroupObject.GetSubObject("boot-images/") 389 result, err := bootImagesObject.CallGet("", nil) 390 if err != nil { 391 return nil, err 392 } 393 list, err := result.GetArray() 394 if err != nil { 395 return nil, err 396 } 397 var bootImages []bootImage 398 for _, obj := range list { 399 bootimage, err := obj.GetMap() 400 if err != nil { 401 return nil, err 402 } 403 arch, err := bootimage["architecture"].GetString() 404 if err != nil { 405 return nil, err 406 } 407 release, err := bootimage["release"].GetString() 408 if err != nil { 409 return nil, err 410 } 411 bootImages = append(bootImages, bootImage{ 412 architecture: arch, 413 release: release, 414 }) 415 } 416 return bootImages, nil 417 } 418 419 // nodeArchitectures returns the architectures of all 420 // available nodes in the system. 421 // 422 // Note: this should only be used if we cannot query 423 // boot-images. 424 func (env *maasEnviron) nodeArchitectures() ([]string, error) { 425 filter := make(url.Values) 426 filter.Add("status", gomaasapi.NodeStatusDeclared) 427 filter.Add("status", gomaasapi.NodeStatusCommissioning) 428 filter.Add("status", gomaasapi.NodeStatusReady) 429 filter.Add("status", gomaasapi.NodeStatusReserved) 430 filter.Add("status", gomaasapi.NodeStatusAllocated) 431 // This is fine - nodeArchitectures is only used in MAAS 1 cases. 432 allInstances, err := env.instances1(filter) 433 if err != nil { 434 return nil, err 435 } 436 architectures := make(set.Strings) 437 for _, inst := range allInstances { 438 inst := inst.(*maas1Instance) 439 arch, _, err := inst.architecture() 440 if err != nil { 441 return nil, err 442 } 443 architectures.Add(arch) 444 } 445 // TODO(dfc) why is this sorted 446 return architectures.SortedValues(), nil 447 } 448 449 type maasAvailabilityZone struct { 450 name string 451 } 452 453 func (z maasAvailabilityZone) Name() string { 454 return z.name 455 } 456 457 func (z maasAvailabilityZone) Available() bool { 458 // MAAS' physical zone attributes only include name and description; 459 // there is no concept of availability. 460 return true 461 } 462 463 // AvailabilityZones returns a slice of availability zones 464 // for the configured region. 465 func (e *maasEnviron) AvailabilityZones() ([]common.AvailabilityZone, error) { 466 e.availabilityZonesMutex.Lock() 467 defer e.availabilityZonesMutex.Unlock() 468 if e.availabilityZones == nil { 469 var availabilityZones []common.AvailabilityZone 470 var err error 471 if e.usingMAAS2() { 472 availabilityZones, err = e.availabilityZones2() 473 if err != nil { 474 return nil, errors.Trace(err) 475 } 476 } else { 477 availabilityZones, err = e.availabilityZones1() 478 if err != nil { 479 return nil, errors.Trace(err) 480 } 481 } 482 e.availabilityZones = availabilityZones 483 } 484 return e.availabilityZones, nil 485 } 486 487 func (e *maasEnviron) availabilityZones1() ([]common.AvailabilityZone, error) { 488 zonesObject := e.getMAASClient().GetSubObject("zones") 489 result, err := zonesObject.CallGet("", nil) 490 if err, ok := errors.Cause(err).(gomaasapi.ServerError); ok && err.StatusCode == http.StatusNotFound { 491 return nil, errors.NewNotImplemented(nil, "the MAAS server does not support zones") 492 } 493 if err != nil { 494 return nil, errors.Annotate(err, "cannot query ") 495 } 496 list, err := result.GetArray() 497 if err != nil { 498 return nil, err 499 } 500 logger.Debugf("availability zones: %+v", list) 501 availabilityZones := make([]common.AvailabilityZone, len(list)) 502 for i, obj := range list { 503 zone, err := obj.GetMap() 504 if err != nil { 505 return nil, err 506 } 507 name, err := zone["name"].GetString() 508 if err != nil { 509 return nil, err 510 } 511 availabilityZones[i] = maasAvailabilityZone{name} 512 } 513 return availabilityZones, nil 514 } 515 516 func (e *maasEnviron) availabilityZones2() ([]common.AvailabilityZone, error) { 517 zones, err := e.maasController.Zones() 518 if err != nil { 519 return nil, errors.Trace(err) 520 } 521 availabilityZones := make([]common.AvailabilityZone, len(zones)) 522 for i, zone := range zones { 523 availabilityZones[i] = maasAvailabilityZone{zone.Name()} 524 } 525 return availabilityZones, nil 526 527 } 528 529 // InstanceAvailabilityZoneNames returns the availability zone names for each 530 // of the specified instances. 531 func (e *maasEnviron) InstanceAvailabilityZoneNames(ids []instance.Id) ([]string, error) { 532 instances, err := e.Instances(ids) 533 if err != nil && err != environs.ErrPartialInstances { 534 return nil, err 535 } 536 zones := make([]string, len(instances)) 537 for i, inst := range instances { 538 if inst == nil { 539 continue 540 } 541 z, err := inst.(maasInstance).zone() 542 if err != nil { 543 logger.Errorf("could not get availability zone %v", err) 544 continue 545 } 546 zones[i] = z 547 } 548 return zones, nil 549 } 550 551 type maasPlacement struct { 552 nodeName string 553 zoneName string 554 } 555 556 func (e *maasEnviron) parsePlacement(placement string) (*maasPlacement, error) { 557 pos := strings.IndexRune(placement, '=') 558 if pos == -1 { 559 // If there's no '=' delimiter, assume it's a node name. 560 return &maasPlacement{nodeName: placement}, nil 561 } 562 switch key, value := placement[:pos], placement[pos+1:]; key { 563 case "zone": 564 availabilityZone := value 565 zones, err := e.AvailabilityZones() 566 if err != nil { 567 return nil, err 568 } 569 for _, z := range zones { 570 if z.Name() == availabilityZone { 571 return &maasPlacement{zoneName: availabilityZone}, nil 572 } 573 } 574 return nil, errors.Errorf("invalid availability zone %q", availabilityZone) 575 } 576 return nil, errors.Errorf("unknown placement directive: %v", placement) 577 } 578 579 func (env *maasEnviron) PrecheckInstance(series string, cons constraints.Value, placement string) error { 580 if placement == "" { 581 return nil 582 } 583 _, err := env.parsePlacement(placement) 584 return err 585 } 586 587 const ( 588 capNetworkDeploymentUbuntu = "network-deployment-ubuntu" 589 ) 590 591 // getCapabilities asks the MAAS server for its capabilities, if 592 // supported by the server. 593 func getCapabilities(client *gomaasapi.MAASObject) (set.Strings, error) { 594 caps := make(set.Strings) 595 var result gomaasapi.JSONObject 596 var err error 597 598 for a := shortAttempt.Start(); a.Next(); { 599 version := client.GetSubObject("version/") 600 result, err = version.CallGet("", nil) 601 if err != nil { 602 if err, ok := errors.Cause(err).(gomaasapi.ServerError); ok && err.StatusCode == 404 { 603 return caps, errors.NotSupportedf("MAAS version 1.9 or more recent is required") 604 } 605 } else { 606 break 607 } 608 } 609 if err != nil { 610 return caps, err 611 } 612 info, err := result.GetMap() 613 if err != nil { 614 return caps, err 615 } 616 capsObj, ok := info["capabilities"] 617 if !ok { 618 return caps, fmt.Errorf("MAAS does not report capabilities") 619 } 620 items, err := capsObj.GetArray() 621 if err != nil { 622 return caps, err 623 } 624 for _, item := range items { 625 val, err := item.GetString() 626 if err != nil { 627 return set.NewStrings(), err 628 } 629 caps.Add(val) 630 } 631 return caps, nil 632 } 633 634 // getMAASClient returns a MAAS client object to use for a request, in a 635 // lock-protected fashion. 636 func (env *maasEnviron) getMAASClient() *gomaasapi.MAASObject { 637 env.ecfgMutex.Lock() 638 defer env.ecfgMutex.Unlock() 639 640 return env.maasClientUnlocked 641 } 642 643 var dashSuffix = regexp.MustCompile("^(.*)-\\d+$") 644 645 func spaceNamesToSpaceInfo(spaces []string, spaceMap map[string]network.SpaceInfo) ([]network.SpaceInfo, error) { 646 spaceInfos := []network.SpaceInfo{} 647 for _, name := range spaces { 648 info, ok := spaceMap[name] 649 if !ok { 650 matches := dashSuffix.FindAllStringSubmatch(name, 1) 651 if matches == nil { 652 return nil, errors.Errorf("unrecognised space in constraint %q", name) 653 } 654 // A -number was added to the space name when we 655 // converted to a juju name, we found 656 info, ok = spaceMap[matches[0][1]] 657 if !ok { 658 return nil, errors.Errorf("unrecognised space in constraint %q", name) 659 } 660 } 661 spaceInfos = append(spaceInfos, info) 662 } 663 return spaceInfos, nil 664 } 665 666 func (environ *maasEnviron) buildSpaceMap() (map[string]network.SpaceInfo, error) { 667 spaces, err := environ.Spaces() 668 if err != nil { 669 return nil, errors.Trace(err) 670 } 671 spaceMap := make(map[string]network.SpaceInfo) 672 empty := set.Strings{} 673 for _, space := range spaces { 674 jujuName := network.ConvertSpaceName(space.Name, empty) 675 spaceMap[jujuName] = space 676 } 677 return spaceMap, nil 678 } 679 680 func (environ *maasEnviron) spaceNamesToSpaceInfo(positiveSpaces, negativeSpaces []string) ([]network.SpaceInfo, []network.SpaceInfo, error) { 681 spaceMap, err := environ.buildSpaceMap() 682 if err != nil { 683 return nil, nil, errors.Trace(err) 684 } 685 686 positiveSpaceIds, err := spaceNamesToSpaceInfo(positiveSpaces, spaceMap) 687 if err != nil { 688 return nil, nil, errors.Trace(err) 689 } 690 negativeSpaceIds, err := spaceNamesToSpaceInfo(negativeSpaces, spaceMap) 691 if err != nil { 692 return nil, nil, errors.Trace(err) 693 } 694 return positiveSpaceIds, negativeSpaceIds, nil 695 } 696 697 // acquireNode2 allocates a machine from MAAS2. 698 func (environ *maasEnviron) acquireNode2( 699 nodeName, zoneName string, 700 cons constraints.Value, 701 interfaces []interfaceBinding, 702 volumes []volumeInfo, 703 ) (maasInstance, error) { 704 acquireParams := convertConstraints2(cons) 705 positiveSpaceNames, negativeSpaceNames := convertSpacesFromConstraints(cons.Spaces) 706 positiveSpaces, negativeSpaces, err := environ.spaceNamesToSpaceInfo(positiveSpaceNames, negativeSpaceNames) 707 // If spaces aren't supported the constraints should be empty anyway. 708 if err != nil && !errors.IsNotSupported(err) { 709 return nil, errors.Trace(err) 710 } 711 err = addInterfaces2(&acquireParams, interfaces, positiveSpaces, negativeSpaces) 712 if err != nil { 713 return nil, errors.Trace(err) 714 } 715 addStorage2(&acquireParams, volumes) 716 acquireParams.AgentName = environ.ecfg().maasAgentName() 717 if zoneName != "" { 718 acquireParams.Zone = zoneName 719 } 720 if nodeName != "" { 721 acquireParams.Hostname = nodeName 722 } else if cons.Arch == nil { 723 logger.Warningf( 724 "no architecture was specified, acquiring an arbitrary node", 725 ) 726 } 727 machine, constraintMatches, err := environ.maasController.AllocateMachine(acquireParams) 728 729 if err != nil { 730 return nil, errors.Trace(err) 731 } 732 return &maas2Instance{machine, constraintMatches}, nil 733 } 734 735 // acquireNode allocates a node from the MAAS. 736 func (environ *maasEnviron) acquireNode( 737 nodeName, zoneName string, 738 cons constraints.Value, 739 interfaces []interfaceBinding, 740 volumes []volumeInfo, 741 ) (gomaasapi.MAASObject, error) { 742 743 acquireParams := convertConstraints(cons) 744 positiveSpaceNames, negativeSpaceNames := convertSpacesFromConstraints(cons.Spaces) 745 positiveSpaces, negativeSpaces, err := environ.spaceNamesToSpaceInfo(positiveSpaceNames, negativeSpaceNames) 746 // If spaces aren't supported the constraints should be empty anyway. 747 if err != nil && !errors.IsNotSupported(err) { 748 return gomaasapi.MAASObject{}, errors.Trace(err) 749 } 750 err = addInterfaces(acquireParams, interfaces, positiveSpaces, negativeSpaces) 751 if err != nil { 752 return gomaasapi.MAASObject{}, errors.Trace(err) 753 } 754 addStorage(acquireParams, volumes) 755 acquireParams.Add("agent_name", environ.ecfg().maasAgentName()) 756 if zoneName != "" { 757 acquireParams.Add("zone", zoneName) 758 } 759 if nodeName != "" { 760 acquireParams.Add("name", nodeName) 761 } else if cons.Arch == nil { 762 // TODO(axw) 2014-08-18 #1358219 763 // We should be requesting preferred 764 // architectures if unspecified, like 765 // in the other providers. 766 // 767 // This is slightly complicated in MAAS 768 // as there are a finite number of each 769 // architecture; preference may also 770 // conflict with other constraints, such 771 // as tags. Thus, a preference becomes a 772 // demand (which may fail) if not handled 773 // properly. 774 logger.Warningf( 775 "no architecture was specified, acquiring an arbitrary node", 776 ) 777 } 778 779 var result gomaasapi.JSONObject 780 for a := shortAttempt.Start(); a.Next(); { 781 client := environ.getMAASClient().GetSubObject("nodes/") 782 logger.Tracef("calling acquire with params: %+v", acquireParams) 783 result, err = client.CallPost("acquire", acquireParams) 784 if err == nil { 785 break 786 } 787 } 788 if err != nil { 789 return gomaasapi.MAASObject{}, err 790 } 791 node, err := result.GetMAASObject() 792 if err != nil { 793 err := errors.Annotate(err, "unexpected result from 'acquire' on MAAS API") 794 return gomaasapi.MAASObject{}, err 795 } 796 return node, nil 797 } 798 799 // startNode installs and boots a node. 800 func (environ *maasEnviron) startNode(node gomaasapi.MAASObject, series string, userdata []byte) (*gomaasapi.MAASObject, error) { 801 params := url.Values{ 802 "distro_series": {series}, 803 "user_data": {string(userdata)}, 804 } 805 // Initialize err to a non-nil value as a sentinel for the following 806 // loop. 807 err := fmt.Errorf("(no error)") 808 var result gomaasapi.JSONObject 809 for a := shortAttempt.Start(); a.Next() && err != nil; { 810 result, err = node.CallPost("start", params) 811 if err == nil { 812 break 813 } 814 } 815 816 if err == nil { 817 var startedNode gomaasapi.MAASObject 818 startedNode, err = result.GetMAASObject() 819 if err != nil { 820 logger.Errorf("cannot process API response after successfully starting node: %v", err) 821 return nil, err 822 } 823 return &startedNode, nil 824 } 825 return nil, err 826 } 827 828 func (environ *maasEnviron) startNode2(node maas2Instance, series string, userdata []byte) (*maas2Instance, error) { 829 err := node.machine.Start(gomaasapi.StartArgs{DistroSeries: series, UserData: string(userdata)}) 830 if err != nil { 831 return nil, errors.Trace(err) 832 } 833 // Machine.Start updates the machine in-place when it succeeds. 834 return &maas2Instance{machine: node.machine}, nil 835 836 } 837 838 // DistributeInstances implements the state.InstanceDistributor policy. 839 func (e *maasEnviron) DistributeInstances(candidates, distributionGroup []instance.Id) ([]instance.Id, error) { 840 return common.DistributeInstances(e, candidates, distributionGroup) 841 } 842 843 var availabilityZoneAllocations = common.AvailabilityZoneAllocations 844 845 // MaintainInstance is specified in the InstanceBroker interface. 846 func (*maasEnviron) MaintainInstance(args environs.StartInstanceParams) error { 847 return nil 848 } 849 850 // StartInstance is specified in the InstanceBroker interface. 851 func (environ *maasEnviron) StartInstance(args environs.StartInstanceParams) ( 852 *environs.StartInstanceResult, error, 853 ) { 854 var availabilityZones []string 855 var nodeName string 856 if args.Placement != "" { 857 placement, err := environ.parsePlacement(args.Placement) 858 if err != nil { 859 return nil, err 860 } 861 switch { 862 case placement.zoneName != "": 863 availabilityZones = append(availabilityZones, placement.zoneName) 864 default: 865 nodeName = placement.nodeName 866 } 867 } 868 869 // If no placement is specified, then automatically spread across 870 // the known zones for optimal spread across the instance distribution 871 // group. 872 if args.Placement == "" { 873 var group []instance.Id 874 var err error 875 if args.DistributionGroup != nil { 876 group, err = args.DistributionGroup() 877 if err != nil { 878 return nil, errors.Annotate(err, "cannot get distribution group") 879 } 880 } 881 zoneInstances, err := availabilityZoneAllocations(environ, group) 882 // TODO (mfoord): this branch is for old versions of MAAS and 883 // can be removed, but this means fixing tests. 884 if errors.IsNotImplemented(err) { 885 // Availability zones are an extension, so we may get a 886 // not implemented error; ignore these. 887 } else if err != nil { 888 return nil, errors.Annotate(err, "cannot get availability zone allocations") 889 } else if len(zoneInstances) > 0 { 890 for _, z := range zoneInstances { 891 availabilityZones = append(availabilityZones, z.ZoneName) 892 } 893 } 894 } 895 if len(availabilityZones) == 0 { 896 availabilityZones = []string{""} 897 } 898 899 // Storage. 900 volumes, err := buildMAASVolumeParameters(args.Volumes, args.Constraints) 901 if err != nil { 902 return nil, errors.Annotate(err, "invalid volume parameters") 903 } 904 905 var interfaceBindings []interfaceBinding 906 if len(args.EndpointBindings) != 0 { 907 for endpoint, spaceProviderID := range args.EndpointBindings { 908 interfaceBindings = append(interfaceBindings, interfaceBinding{ 909 Name: endpoint, 910 SpaceProviderId: string(spaceProviderID), 911 }) 912 } 913 } 914 snArgs := selectNodeArgs{ 915 Constraints: args.Constraints, 916 AvailabilityZones: availabilityZones, 917 NodeName: nodeName, 918 Interfaces: interfaceBindings, 919 Volumes: volumes, 920 } 921 var inst maasInstance 922 if !environ.usingMAAS2() { 923 selectedNode, err := environ.selectNode(snArgs) 924 if err != nil { 925 return nil, errors.Errorf("cannot run instances: %v", err) 926 } 927 928 inst = &maas1Instance{ 929 maasObject: selectedNode, 930 environ: environ, 931 statusGetter: environ.deploymentStatusOne, 932 } 933 } else { 934 inst, err = environ.selectNode2(snArgs) 935 if err != nil { 936 return nil, errors.Annotatef(err, "cannot run instances") 937 } 938 } 939 defer func() { 940 if err != nil { 941 if err := environ.StopInstances(inst.Id()); err != nil { 942 logger.Errorf("error releasing failed instance: %v", err) 943 } 944 } 945 }() 946 947 hc, err := inst.hardwareCharacteristics() 948 if err != nil { 949 return nil, err 950 } 951 952 series := args.Tools.OneSeries() 953 selectedTools, err := args.Tools.Match(tools.Filter{ 954 Arch: *hc.Arch, 955 }) 956 if err != nil { 957 return nil, errors.Trace(err) 958 } 959 if err := args.InstanceConfig.SetTools(selectedTools); err != nil { 960 return nil, errors.Trace(err) 961 } 962 963 hostname, err := inst.hostname() 964 if err != nil { 965 return nil, err 966 } 967 968 if err := instancecfg.FinishInstanceConfig(args.InstanceConfig, environ.Config()); err != nil { 969 return nil, errors.Trace(err) 970 } 971 972 cloudcfg, err := environ.newCloudinitConfig(hostname, series) 973 if err != nil { 974 return nil, errors.Trace(err) 975 } 976 userdata, err := providerinit.ComposeUserData(args.InstanceConfig, cloudcfg, MAASRenderer{}) 977 if err != nil { 978 return nil, errors.Annotatef(err, "could not compose userdata for bootstrap node") 979 } 980 logger.Debugf("maas user data; %d bytes", len(userdata)) 981 982 subnetsMap, err := environ.subnetToSpaceIds() 983 if err != nil { 984 return nil, errors.Trace(err) 985 } 986 var interfaces []network.InterfaceInfo 987 if !environ.usingMAAS2() { 988 inst1 := inst.(*maas1Instance) 989 startedNode, err := environ.startNode(*inst1.maasObject, series, userdata) 990 if err != nil { 991 return nil, errors.Trace(err) 992 } 993 // Once the instance has started the response should contain the 994 // assigned IP addresses, even when NICs are set to "auto" instead of 995 // "static". So instead of selectedNode, which only contains the 996 // acquire-time details (no IP addresses for NICs set to "auto" vs 997 // "static"), we use the up-to-date startedNode response to get the 998 // interfaces. 999 interfaces, err = maasObjectNetworkInterfaces(startedNode, subnetsMap) 1000 if err != nil { 1001 return nil, errors.Trace(err) 1002 } 1003 } else { 1004 startedInst, err := environ.startNode2(*inst.(*maas2Instance), series, userdata) 1005 if err != nil { 1006 return nil, errors.Trace(err) 1007 } 1008 interfaces, err = maas2NetworkInterfaces(startedInst, subnetsMap) 1009 if err != nil { 1010 return nil, errors.Trace(err) 1011 } 1012 } 1013 logger.Debugf("started instance %q", inst.Id()) 1014 1015 if multiwatcher.AnyJobNeedsState(args.InstanceConfig.Jobs...) { 1016 if err := common.AddStateInstance(environ.Storage(), inst.Id()); err != nil { 1017 logger.Errorf("could not record instance in provider-state: %v", err) 1018 } 1019 } 1020 1021 requestedVolumes := make([]names.VolumeTag, len(args.Volumes)) 1022 for i, v := range args.Volumes { 1023 requestedVolumes[i] = v.Tag 1024 } 1025 resultVolumes, resultAttachments, err := inst.volumes( 1026 names.NewMachineTag(args.InstanceConfig.MachineId), 1027 requestedVolumes, 1028 ) 1029 if err != nil { 1030 return nil, err 1031 } 1032 if len(resultVolumes) != len(requestedVolumes) { 1033 err = errors.Errorf("requested %v storage volumes. %v returned.", len(requestedVolumes), len(resultVolumes)) 1034 return nil, err 1035 } 1036 1037 return &environs.StartInstanceResult{ 1038 Instance: inst, 1039 Hardware: hc, 1040 NetworkInfo: interfaces, 1041 Volumes: resultVolumes, 1042 VolumeAttachments: resultAttachments, 1043 }, nil 1044 } 1045 1046 // Override for testing. 1047 var nodeDeploymentTimeout = func(environ *maasEnviron) time.Duration { 1048 sshTimeouts := environ.Config().BootstrapSSHOpts() 1049 return sshTimeouts.Timeout 1050 } 1051 1052 func (environ *maasEnviron) waitForNodeDeployment(id instance.Id) error { 1053 if environ.usingMAAS2() { 1054 return environ.waitForNodeDeployment2(id) 1055 } 1056 systemId := extractSystemId(id) 1057 1058 longAttempt := utils.AttemptStrategy{ 1059 Delay: 10 * time.Second, 1060 Total: nodeDeploymentTimeout(environ), 1061 } 1062 1063 for a := longAttempt.Start(); a.Next(); { 1064 statusValues, err := environ.deploymentStatus(id) 1065 if errors.IsNotImplemented(err) { 1066 return nil 1067 } 1068 if err != nil { 1069 return errors.Trace(err) 1070 } 1071 if statusValues[systemId] == "Deployed" { 1072 return nil 1073 } 1074 if statusValues[systemId] == "Failed deployment" { 1075 return errors.Errorf("instance %q failed to deploy", id) 1076 } 1077 } 1078 return errors.Errorf("instance %q is started but not deployed", id) 1079 } 1080 1081 func (environ *maasEnviron) waitForNodeDeployment2(id instance.Id) error { 1082 longAttempt := utils.AttemptStrategy{ 1083 Delay: 10 * time.Second, 1084 Total: nodeDeploymentTimeout(environ), 1085 } 1086 1087 for a := longAttempt.Start(); a.Next(); { 1088 machine, err := environ.getInstance(id) 1089 if err != nil { 1090 return errors.Trace(err) 1091 } 1092 stat := machine.Status() 1093 if stat.Status == status.StatusRunning { 1094 return nil 1095 } 1096 if stat.Status == status.StatusProvisioningError { 1097 return errors.Errorf("instance %q failed to deploy", id) 1098 1099 } 1100 } 1101 return errors.Errorf("instance %q is started but not deployed", id) 1102 } 1103 1104 func (environ *maasEnviron) deploymentStatusOne(id instance.Id) (string, string) { 1105 results, err := environ.deploymentStatus(id) 1106 if err != nil { 1107 return "", "" 1108 } 1109 systemId := extractSystemId(id) 1110 substatus := environ.getDeploymentSubstatus(systemId) 1111 return results[systemId], substatus 1112 } 1113 1114 func (environ *maasEnviron) getDeploymentSubstatus(systemId string) string { 1115 nodesAPI := environ.getMAASClient().GetSubObject("nodes") 1116 result, err := nodesAPI.CallGet("list", nil) 1117 if err != nil { 1118 return "" 1119 } 1120 slices, err := result.GetArray() 1121 if err != nil { 1122 return "" 1123 } 1124 for _, slice := range slices { 1125 resultMap, err := slice.GetMap() 1126 if err != nil { 1127 continue 1128 } 1129 sysId, err := resultMap["system_id"].GetString() 1130 if err != nil { 1131 continue 1132 } 1133 if sysId == systemId { 1134 message, err := resultMap["substatus_message"].GetString() 1135 if err != nil { 1136 logger.Warningf("could not get string for substatus_message: %v", resultMap["substatus_message"]) 1137 return "" 1138 } 1139 return message 1140 } 1141 } 1142 1143 return "" 1144 } 1145 1146 // deploymentStatus returns the deployment state of MAAS instances with 1147 // the specified Juju instance ids. 1148 // Note: the result is a map of MAAS systemId to state. 1149 func (environ *maasEnviron) deploymentStatus(ids ...instance.Id) (map[string]string, error) { 1150 nodesAPI := environ.getMAASClient().GetSubObject("nodes") 1151 result, err := DeploymentStatusCall(nodesAPI, ids...) 1152 if err != nil { 1153 if err, ok := errors.Cause(err).(gomaasapi.ServerError); ok && err.StatusCode == http.StatusBadRequest { 1154 return nil, errors.NewNotImplemented(err, "deployment status") 1155 } 1156 return nil, errors.Trace(err) 1157 } 1158 resultMap, err := result.GetMap() 1159 if err != nil { 1160 return nil, errors.Trace(err) 1161 } 1162 statusValues := make(map[string]string) 1163 for systemId, jsonValue := range resultMap { 1164 status, err := jsonValue.GetString() 1165 if err != nil { 1166 return nil, errors.Trace(err) 1167 } 1168 statusValues[systemId] = status 1169 } 1170 return statusValues, nil 1171 } 1172 1173 func deploymentStatusCall(nodes gomaasapi.MAASObject, ids ...instance.Id) (gomaasapi.JSONObject, error) { 1174 filter := getSystemIdValues("nodes", ids) 1175 return nodes.CallGet("deployment_status", filter) 1176 } 1177 1178 type selectNodeArgs struct { 1179 AvailabilityZones []string 1180 NodeName string 1181 Constraints constraints.Value 1182 Interfaces []interfaceBinding 1183 Volumes []volumeInfo 1184 } 1185 1186 func (environ *maasEnviron) selectNode(args selectNodeArgs) (*gomaasapi.MAASObject, error) { 1187 var err error 1188 var node gomaasapi.MAASObject 1189 1190 for i, zoneName := range args.AvailabilityZones { 1191 node, err = environ.acquireNode( 1192 args.NodeName, 1193 zoneName, 1194 args.Constraints, 1195 args.Interfaces, 1196 args.Volumes, 1197 ) 1198 1199 if err, ok := errors.Cause(err).(gomaasapi.ServerError); ok && err.StatusCode == http.StatusConflict { 1200 if i+1 < len(args.AvailabilityZones) { 1201 logger.Infof("could not acquire a node in zone %q, trying another zone", zoneName) 1202 continue 1203 } 1204 } 1205 if err != nil { 1206 return nil, errors.Errorf("cannot run instances: %v", err) 1207 } 1208 // Since a return at the end of the function is required 1209 // just break here. 1210 break 1211 } 1212 return &node, nil 1213 } 1214 1215 func (environ *maasEnviron) selectNode2(args selectNodeArgs) (maasInstance, error) { 1216 var err error 1217 var inst maasInstance 1218 1219 for i, zoneName := range args.AvailabilityZones { 1220 inst, err = environ.acquireNode2( 1221 args.NodeName, 1222 zoneName, 1223 args.Constraints, 1224 args.Interfaces, 1225 args.Volumes, 1226 ) 1227 1228 if gomaasapi.IsNoMatchError(err) { 1229 if i+1 < len(args.AvailabilityZones) { 1230 logger.Infof("could not acquire a node in zone %q, trying another zone", zoneName) 1231 continue 1232 } 1233 } 1234 if err != nil { 1235 return nil, errors.Annotatef(err, "cannot run instance") 1236 } 1237 // Since a return at the end of the function is required 1238 // just break here. 1239 break 1240 } 1241 return inst, nil 1242 } 1243 1244 // setupJujuNetworking returns a string representing the script to run 1245 // in order to prepare the Juju-specific networking config on a node. 1246 func setupJujuNetworking() string { 1247 // For ubuntu series < xenial we prefer python2 over python3 1248 // as we don't want to invalidate lots of testing against 1249 // known cloud-image contents. A summary of Ubuntu releases 1250 // and python inclusion in the default install of Ubuntu 1251 // Server is as follows: 1252 // 1253 // 12.04 precise: python 2 (2.7.3) 1254 // 14.04 trusty: python 2 (2.7.5) and python3 (3.4.0) 1255 // 14.10 utopic: python 2 (2.7.8) and python3 (3.4.2) 1256 // 15.04 vivid: python 2 (2.7.9) and python3 (3.4.3) 1257 // 15.10 wily: python 2 (2.7.9) and python3 (3.4.3) 1258 // 16.04 xenial: python 3 only (3.5.1) 1259 // 1260 // going forward: python 3 only 1261 1262 return fmt.Sprintf(` 1263 trap 'rm -f %[1]q' EXIT 1264 1265 if [ -x /usr/bin/python2 ]; then 1266 juju_networking_preferred_python_binary=/usr/bin/python2 1267 elif [ -x /usr/bin/python3 ]; then 1268 juju_networking_preferred_python_binary=/usr/bin/python3 1269 elif [ -x /usr/bin/python ]; then 1270 juju_networking_preferred_python_binary=/usr/bin/python 1271 fi 1272 1273 if [ ! -z "${juju_networking_preferred_python_binary:-}" ]; then 1274 if [ -f %[1]q ]; then 1275 # We are sharing this code between master, maas-spaces2 and 1.25. 1276 # For the moment we want master and 1.25 to not bridge all interfaces. 1277 # This setting allows us to easily switch the behaviour when merging 1278 # the code between those various branches. 1279 juju_bridge_all_interfaces=1 1280 if [ $juju_bridge_all_interfaces -eq 1 ]; then 1281 $juju_networking_preferred_python_binary %[1]q --bridge-prefix=%[2]q --one-time-backup --activate %[4]q 1282 else 1283 juju_ipv4_interface_to_bridge=$(ip -4 route list exact default | head -n1 | cut -d' ' -f5) 1284 $juju_networking_preferred_python_binary %[1]q --bridge-name=%[3]q --interface-to-bridge="${juju_ipv4_interface_to_bridge:-unknown}" --one-time-backup --activate %[4]q 1285 fi 1286 fi 1287 else 1288 echo "error: no Python installation found; cannot run Juju's bridge script" 1289 fi`, 1290 bridgeScriptPath, 1291 instancecfg.DefaultBridgePrefix, 1292 instancecfg.DefaultBridgeName, 1293 "/etc/network/interfaces") 1294 } 1295 1296 func renderEtcNetworkInterfacesScript() string { 1297 return setupJujuNetworking() 1298 } 1299 1300 // newCloudinitConfig creates a cloudinit.Config structure suitable as a base 1301 // for initialising a MAAS node. 1302 func (environ *maasEnviron) newCloudinitConfig(hostname, forSeries string) (cloudinit.CloudConfig, error) { 1303 cloudcfg, err := cloudinit.New(forSeries) 1304 if err != nil { 1305 return nil, err 1306 } 1307 1308 info := machineInfo{hostname} 1309 runCmd, err := info.cloudinitRunCmd(cloudcfg) 1310 if err != nil { 1311 return nil, errors.Trace(err) 1312 } 1313 1314 operatingSystem, err := series.GetOSFromSeries(forSeries) 1315 if err != nil { 1316 return nil, errors.Trace(err) 1317 } 1318 switch operatingSystem { 1319 case os.Windows: 1320 cloudcfg.AddScripts(runCmd) 1321 case os.Ubuntu: 1322 cloudcfg.SetSystemUpdate(true) 1323 cloudcfg.AddScripts("set -xe", runCmd) 1324 // DisableNetworkManagement can still disable the bridge(s) creation. 1325 if on, set := environ.Config().DisableNetworkManagement(); on && set { 1326 logger.Infof( 1327 "network management disabled - not using %q bridge for containers", 1328 instancecfg.DefaultBridgeName, 1329 ) 1330 break 1331 } 1332 cloudcfg.AddPackage("bridge-utils") 1333 cloudcfg.AddBootTextFile(bridgeScriptPath, bridgeScriptPython, 0755) 1334 cloudcfg.AddScripts(setupJujuNetworking()) 1335 } 1336 return cloudcfg, nil 1337 } 1338 1339 func (environ *maasEnviron) releaseNodes1(nodes gomaasapi.MAASObject, ids url.Values, recurse bool) error { 1340 err := ReleaseNodes(nodes, ids) 1341 if err == nil { 1342 return nil 1343 } 1344 maasErr, ok := errors.Cause(err).(gomaasapi.ServerError) 1345 if !ok { 1346 return errors.Annotate(err, "cannot release nodes") 1347 } 1348 1349 // StatusCode 409 means a node couldn't be released due to 1350 // a state conflict. Likely it's already released or disk 1351 // erasing. We're assuming an error of 409 *only* means it's 1352 // safe to assume the instance is already released. 1353 // MaaS also releases (or attempts) all nodes, and raises 1354 // a single error on failure. So even with an error 409, all 1355 // nodes have been released. 1356 if maasErr.StatusCode == 409 { 1357 logger.Infof("ignoring error while releasing nodes (%v); all nodes released OK", err) 1358 return nil 1359 } 1360 1361 // a status code of 400, 403 or 404 means one of the nodes 1362 // couldn't be found and none have been released. We have 1363 // to release all the ones we can individually. 1364 if maasErr.StatusCode != 400 && maasErr.StatusCode != 403 && maasErr.StatusCode != 404 { 1365 return errors.Annotate(err, "cannot release nodes") 1366 } 1367 if !recurse { 1368 // this node has already been released and we're golden 1369 return nil 1370 } 1371 1372 var lastErr error 1373 for _, id := range ids["nodes"] { 1374 idFilter := url.Values{} 1375 idFilter.Add("nodes", id) 1376 err := environ.releaseNodes1(nodes, idFilter, false) 1377 if err != nil { 1378 lastErr = err 1379 logger.Errorf("error while releasing node %v (%v)", id, err) 1380 } 1381 } 1382 return errors.Trace(lastErr) 1383 1384 } 1385 1386 func (environ *maasEnviron) releaseNodes2(ids []instance.Id, recurse bool) error { 1387 args := gomaasapi.ReleaseMachinesArgs{ 1388 SystemIDs: instanceIdsToSystemIDs(ids), 1389 Comment: "Released by Juju MAAS provider", 1390 } 1391 err := environ.maasController.ReleaseMachines(args) 1392 1393 switch { 1394 case err == nil: 1395 return nil 1396 case gomaasapi.IsCannotCompleteError(err): 1397 // CannotCompleteError means a node couldn't be released due to 1398 // a state conflict. Likely it's already released or disk 1399 // erasing. We're assuming this error *only* means it's 1400 // safe to assume the instance is already released. 1401 // MaaS also releases (or attempts) all nodes, and raises 1402 // a single error on failure. So even with an error 409, all 1403 // nodes have been released. 1404 logger.Infof("ignoring error while releasing nodes (%v); all nodes released OK", err) 1405 return nil 1406 case gomaasapi.IsBadRequestError(err), gomaasapi.IsPermissionError(err): 1407 // a status code of 400 or 403 means one of the nodes 1408 // couldn't be found and none have been released. We have to 1409 // release all the ones we can individually. 1410 if !recurse { 1411 // this node has already been released and we're golden 1412 return nil 1413 } 1414 return environ.releaseNodesIndividually(ids) 1415 1416 default: 1417 return errors.Annotatef(err, "cannot release nodes") 1418 } 1419 } 1420 1421 func (environ *maasEnviron) releaseNodesIndividually(ids []instance.Id) error { 1422 var lastErr error 1423 for _, id := range ids { 1424 err := environ.releaseNodes2([]instance.Id{id}, false) 1425 if err != nil { 1426 lastErr = err 1427 logger.Errorf("error while releasing node %v (%v)", id, err) 1428 } 1429 } 1430 return errors.Trace(lastErr) 1431 } 1432 1433 func instanceIdsToSystemIDs(ids []instance.Id) []string { 1434 systemIDs := make([]string, len(ids)) 1435 for index, id := range ids { 1436 systemIDs[index] = string(id) 1437 } 1438 return systemIDs 1439 } 1440 1441 // StopInstances is specified in the InstanceBroker interface. 1442 func (environ *maasEnviron) StopInstances(ids ...instance.Id) error { 1443 // Shortcut to exit quickly if 'instances' is an empty slice or nil. 1444 if len(ids) == 0 { 1445 return nil 1446 } 1447 1448 if environ.usingMAAS2() { 1449 err := environ.releaseNodes2(ids, true) 1450 if err != nil { 1451 return errors.Trace(err) 1452 } 1453 } else { 1454 nodes := environ.getMAASClient().GetSubObject("nodes") 1455 err := environ.releaseNodes1(nodes, getSystemIdValues("nodes", ids), true) 1456 if err != nil { 1457 return errors.Trace(err) 1458 } 1459 } 1460 return common.RemoveStateInstances(environ.Storage(), ids...) 1461 1462 } 1463 1464 // acquireInstances calls the MAAS API to list acquired nodes. 1465 // 1466 // The "ids" slice is a filter for specific instance IDs. 1467 // Due to how this works in the HTTP API, an empty "ids" 1468 // matches all instances (not none as you might expect). 1469 func (environ *maasEnviron) acquiredInstances(ids []instance.Id) ([]instance.Instance, error) { 1470 if !environ.usingMAAS2() { 1471 filter := getSystemIdValues("id", ids) 1472 filter.Add("agent_name", environ.ecfg().maasAgentName()) 1473 return environ.instances1(filter) 1474 } 1475 args := gomaasapi.MachinesArgs{ 1476 AgentName: environ.ecfg().maasAgentName(), 1477 SystemIDs: instanceIdsToSystemIDs(ids), 1478 } 1479 return environ.instances2(args) 1480 } 1481 1482 // instances calls the MAAS API to list nodes matching the given filter. 1483 func (environ *maasEnviron) instances1(filter url.Values) ([]instance.Instance, error) { 1484 nodeListing := environ.getMAASClient().GetSubObject("nodes") 1485 listNodeObjects, err := nodeListing.CallGet("list", filter) 1486 if err != nil { 1487 return nil, err 1488 } 1489 listNodes, err := listNodeObjects.GetArray() 1490 if err != nil { 1491 return nil, err 1492 } 1493 instances := make([]instance.Instance, len(listNodes)) 1494 for index, nodeObj := range listNodes { 1495 node, err := nodeObj.GetMAASObject() 1496 if err != nil { 1497 return nil, err 1498 } 1499 instances[index] = &maas1Instance{ 1500 maasObject: &node, 1501 environ: environ, 1502 statusGetter: environ.deploymentStatusOne, 1503 } 1504 } 1505 return instances, nil 1506 } 1507 1508 func (environ *maasEnviron) instances2(args gomaasapi.MachinesArgs) ([]instance.Instance, error) { 1509 machines, err := environ.maasController.Machines(args) 1510 if err != nil { 1511 return nil, errors.Trace(err) 1512 } 1513 instances := make([]instance.Instance, len(machines)) 1514 for index, machine := range machines { 1515 instances[index] = &maas2Instance{machine: machine} 1516 } 1517 return instances, nil 1518 } 1519 1520 // Instances returns the instance.Instance objects corresponding to the given 1521 // slice of instance.Id. The error is ErrNoInstances if no instances 1522 // were found. 1523 func (environ *maasEnviron) Instances(ids []instance.Id) ([]instance.Instance, error) { 1524 if len(ids) == 0 { 1525 // This would be treated as "return all instances" below, so 1526 // treat it as a special case. 1527 // The interface requires us to return this particular error 1528 // if no instances were found. 1529 return nil, environs.ErrNoInstances 1530 } 1531 instances, err := environ.acquiredInstances(ids) 1532 if err != nil { 1533 return nil, errors.Trace(err) 1534 } 1535 if len(instances) == 0 { 1536 return nil, environs.ErrNoInstances 1537 } 1538 1539 idMap := make(map[instance.Id]instance.Instance) 1540 for _, instance := range instances { 1541 idMap[instance.Id()] = instance 1542 } 1543 1544 missing := false 1545 result := make([]instance.Instance, len(ids)) 1546 for index, id := range ids { 1547 val, ok := idMap[id] 1548 if !ok { 1549 missing = true 1550 continue 1551 } 1552 result[index] = val 1553 } 1554 1555 if missing { 1556 return result, environs.ErrPartialInstances 1557 } 1558 return result, nil 1559 } 1560 1561 // AllocateAddress requests an address to be allocated for the given instance on 1562 // the given network. 1563 func (environ *maasEnviron) AllocateAddress(instId instance.Id, subnetId network.Id, addr *network.Address, macAddress, hostname string) (err error) { 1564 return errors.NotSupportedf("AllocateAddress") 1565 } 1566 1567 // ReleaseAddress releases a specific address previously allocated with 1568 // AllocateAddress. 1569 func (environ *maasEnviron) ReleaseAddress(instId instance.Id, _ network.Id, addr network.Address, macAddress, hostname string) (err error) { 1570 return errors.NotSupportedf("ReleaseAddress") 1571 } 1572 1573 // subnetsFromNode fetches all the subnets for a specific node. 1574 func (environ *maasEnviron) subnetsFromNode(nodeId string) ([]gomaasapi.JSONObject, error) { 1575 client := environ.getMAASClient().GetSubObject("nodes").GetSubObject(nodeId) 1576 json, err := client.CallGet("", nil) 1577 if err != nil { 1578 if maasErr, ok := errors.Cause(err).(gomaasapi.ServerError); ok && maasErr.StatusCode == http.StatusNotFound { 1579 return nil, errors.NotFoundf("intance %q", nodeId) 1580 } 1581 return nil, errors.Trace(err) 1582 } 1583 nodeMap, err := json.GetMap() 1584 if err != nil { 1585 return nil, errors.Trace(err) 1586 } 1587 interfacesArray, err := nodeMap["interface_set"].GetArray() 1588 if err != nil { 1589 return nil, errors.Trace(err) 1590 } 1591 var subnets []gomaasapi.JSONObject 1592 for _, iface := range interfacesArray { 1593 ifaceMap, err := iface.GetMap() 1594 if err != nil { 1595 return nil, errors.Trace(err) 1596 } 1597 linksArray, err := ifaceMap["links"].GetArray() 1598 if err != nil { 1599 return nil, errors.Trace(err) 1600 } 1601 for _, link := range linksArray { 1602 linkMap, err := link.GetMap() 1603 if err != nil { 1604 return nil, errors.Trace(err) 1605 } 1606 subnet, ok := linkMap["subnet"] 1607 if !ok { 1608 return nil, errors.New("subnet not found") 1609 } 1610 subnets = append(subnets, subnet) 1611 } 1612 } 1613 return subnets, nil 1614 } 1615 1616 // Deduce the allocatable portion of the subnet by subtracting the dynamic 1617 // range from the full subnet range. 1618 func (environ *maasEnviron) allocatableRangeForSubnet(cidr string, subnetId string) (net.IP, net.IP, error) { 1619 // Initialize the low and high bounds of the allocatable range to the 1620 // whole CIDR. Reduce the scope of this when we find the dynamic range. 1621 ip, ipnet, err := net.ParseCIDR(cidr) 1622 if err != nil { 1623 return nil, nil, errors.Trace(err) 1624 } 1625 // Skip IPv6 subnets until we can handle them correctly. 1626 if ip.To4() == nil && ip.To16() != nil { 1627 logger.Debugf("ignoring static IP range for IPv6 subnet %q", cidr) 1628 return nil, nil, nil 1629 } 1630 1631 // TODO(mfoord): needs updating to work with IPv6 as well. 1632 lowBound, err := network.IPv4ToDecimal(ip) 1633 if err != nil { 1634 return nil, nil, errors.Trace(err) 1635 } 1636 // Don't include the zero address in the allocatable bounds. 1637 lowBound = lowBound + 1 1638 ones, bits := ipnet.Mask.Size() 1639 zeros := bits - ones 1640 numIPs := uint32(1) << uint32(zeros) 1641 highBound := lowBound + numIPs - 2 1642 1643 client := environ.getMAASClient().GetSubObject("subnets").GetSubObject(subnetId) 1644 1645 json, err := client.CallGet("reserved_ip_ranges", nil) 1646 if err != nil { 1647 return nil, nil, errors.Trace(err) 1648 } 1649 jsonRanges, err := json.GetArray() 1650 if err != nil { 1651 return nil, nil, errors.Trace(err) 1652 } 1653 1654 for _, jsonRange := range jsonRanges { 1655 rangeMap, err := jsonRange.GetMap() 1656 if err != nil { 1657 return nil, nil, errors.Trace(err) 1658 } 1659 purposeArray, err := rangeMap["purpose"].GetArray() 1660 if err != nil { 1661 return nil, nil, errors.Trace(err) 1662 } 1663 found := false 1664 for _, jsonPurpose := range purposeArray { 1665 purpose, err := jsonPurpose.GetString() 1666 if err != nil { 1667 return nil, nil, errors.Trace(err) 1668 } 1669 if purpose == dynamicRange { 1670 found = true 1671 break 1672 } 1673 } 1674 if !found { 1675 // This is not the range we're looking for 1676 continue 1677 } 1678 1679 start, err := rangeMap["start"].GetString() 1680 if err != nil { 1681 return nil, nil, errors.Trace(err) 1682 } 1683 end, err := rangeMap["end"].GetString() 1684 if err != nil { 1685 return nil, nil, errors.Trace(err) 1686 } 1687 dynamicLow, err := network.IPv4ToDecimal(net.ParseIP(start)) 1688 if err != nil { 1689 return nil, nil, errors.Trace(err) 1690 } 1691 dynamicHigh, err := network.IPv4ToDecimal(net.ParseIP(end)) 1692 if err != nil { 1693 return nil, nil, errors.Trace(err) 1694 } 1695 1696 // We pick the larger of the two portions of the subnet around 1697 // the dynamic range. Either ending one below the start of the 1698 // dynamic range or starting one after the end. 1699 above := highBound - dynamicHigh 1700 below := dynamicLow - lowBound 1701 if above > below { 1702 lowBound = dynamicHigh + 1 1703 } else { 1704 highBound = dynamicLow - 1 1705 } 1706 break 1707 } 1708 return network.DecimalToIPv4(lowBound), network.DecimalToIPv4(highBound), nil 1709 } 1710 1711 // subnetFromJson populates a network.SubnetInfo from a gomaasapi.JSONObject 1712 // representing a single subnet. This can come from either the subnets api 1713 // endpoint or the node endpoint. 1714 func (environ *maasEnviron) subnetFromJson(subnet gomaasapi.JSONObject, spaceId network.Id) (network.SubnetInfo, error) { 1715 var subnetInfo network.SubnetInfo 1716 fields, err := subnet.GetMap() 1717 if err != nil { 1718 return subnetInfo, errors.Trace(err) 1719 } 1720 subnetIdFloat, err := fields["id"].GetFloat64() 1721 if err != nil { 1722 return subnetInfo, errors.Annotatef(err, "cannot get subnet Id") 1723 } 1724 subnetId := strconv.Itoa(int(subnetIdFloat)) 1725 cidr, err := fields["cidr"].GetString() 1726 if err != nil { 1727 return subnetInfo, errors.Annotatef(err, "cannot get cidr") 1728 } 1729 vid := 0 1730 vidField, ok := fields["vid"] 1731 if ok && !vidField.IsNil() { 1732 // vid is optional, so assume it's 0 when missing or nil. 1733 vidFloat, err := vidField.GetFloat64() 1734 if err != nil { 1735 return subnetInfo, errors.Errorf("cannot get vlan tag: %v", err) 1736 } 1737 vid = int(vidFloat) 1738 } 1739 allocatableLow, allocatableHigh, err := environ.allocatableRangeForSubnet(cidr, subnetId) 1740 if err != nil { 1741 return subnetInfo, errors.Trace(err) 1742 } 1743 1744 subnetInfo = network.SubnetInfo{ 1745 ProviderId: network.Id(subnetId), 1746 VLANTag: vid, 1747 CIDR: cidr, 1748 SpaceProviderId: spaceId, 1749 AllocatableIPLow: allocatableLow, 1750 AllocatableIPHigh: allocatableHigh, 1751 } 1752 return subnetInfo, nil 1753 } 1754 1755 // filteredSubnets fetches subnets, filtering optionally by nodeId and/or a 1756 // slice of subnetIds. If subnetIds is empty then all subnets for that node are 1757 // fetched. If nodeId is empty, all subnets are returned (filtering by subnetIds 1758 // first, if set). 1759 func (environ *maasEnviron) filteredSubnets(nodeId string, subnetIds []network.Id) ([]network.SubnetInfo, error) { 1760 var jsonNets []gomaasapi.JSONObject 1761 var err error 1762 if nodeId != "" { 1763 jsonNets, err = environ.subnetsFromNode(nodeId) 1764 if err != nil { 1765 return nil, errors.Trace(err) 1766 } 1767 } else { 1768 jsonNets, err = environ.fetchAllSubnets() 1769 if err != nil { 1770 return nil, errors.Trace(err) 1771 } 1772 } 1773 subnetIdSet := make(map[string]bool) 1774 for _, netId := range subnetIds { 1775 subnetIdSet[string(netId)] = false 1776 } 1777 1778 subnetsMap, err := environ.subnetToSpaceIds() 1779 if err != nil { 1780 return nil, errors.Trace(err) 1781 } 1782 1783 subnets := []network.SubnetInfo{} 1784 for _, jsonNet := range jsonNets { 1785 fields, err := jsonNet.GetMap() 1786 if err != nil { 1787 return nil, err 1788 } 1789 subnetIdFloat, err := fields["id"].GetFloat64() 1790 if err != nil { 1791 return nil, errors.Annotatef(err, "cannot get subnet Id: %v") 1792 } 1793 subnetId := strconv.Itoa(int(subnetIdFloat)) 1794 // If we're filtering by subnet id check if this subnet is one 1795 // we're looking for. 1796 if len(subnetIds) != 0 { 1797 _, ok := subnetIdSet[subnetId] 1798 if !ok { 1799 // This id is not what we're looking for. 1800 continue 1801 } 1802 subnetIdSet[subnetId] = true 1803 } 1804 cidr, err := fields["cidr"].GetString() 1805 if err != nil { 1806 return nil, errors.Annotatef(err, "cannot get subnet Id") 1807 } 1808 spaceId, ok := subnetsMap[cidr] 1809 if !ok { 1810 logger.Warningf("unrecognised subnet: %q, setting empty space id", cidr) 1811 spaceId = network.UnknownId 1812 } 1813 1814 subnetInfo, err := environ.subnetFromJson(jsonNet, spaceId) 1815 if err != nil { 1816 return nil, errors.Trace(err) 1817 } 1818 subnets = append(subnets, subnetInfo) 1819 logger.Tracef("found subnet with info %#v", subnetInfo) 1820 } 1821 return subnets, checkNotFound(subnetIdSet) 1822 } 1823 1824 func (environ *maasEnviron) getInstance(instId instance.Id) (instance.Instance, error) { 1825 instances, err := environ.acquiredInstances([]instance.Id{instId}) 1826 if err != nil { 1827 // This path can never trigger on MAAS 2, but MAAS 2 doesn't 1828 // return an error for a machine not found, it just returns 1829 // empty results. The clause below catches that. 1830 if maasErr, ok := errors.Cause(err).(gomaasapi.ServerError); ok && maasErr.StatusCode == http.StatusNotFound { 1831 return nil, errors.NotFoundf("instance %q", instId) 1832 } 1833 return nil, errors.Annotatef(err, "getting instance %q", instId) 1834 } 1835 if len(instances) == 0 { 1836 return nil, errors.NotFoundf("instance %q", instId) 1837 } 1838 inst := instances[0] 1839 return inst, nil 1840 } 1841 1842 // fetchAllSubnets calls the MAAS subnets API to get all subnets and returns the 1843 // JSON response or an error. If capNetworkDeploymentUbuntu is not available, an 1844 // error satisfying errors.IsNotSupported will be returned. 1845 func (environ *maasEnviron) fetchAllSubnets() ([]gomaasapi.JSONObject, error) { 1846 client := environ.getMAASClient().GetSubObject("subnets") 1847 1848 json, err := client.CallGet("", nil) 1849 if err != nil { 1850 return nil, errors.Trace(err) 1851 } 1852 return json.GetArray() 1853 } 1854 1855 // subnetToSpaceIds fetches the spaces from MAAS and builds a map of subnets to 1856 // space ids. 1857 func (environ *maasEnviron) subnetToSpaceIds() (map[string]network.Id, error) { 1858 subnetsMap := make(map[string]network.Id) 1859 spaces, err := environ.Spaces() 1860 if err != nil { 1861 return subnetsMap, errors.Trace(err) 1862 } 1863 for _, space := range spaces { 1864 for _, subnet := range space.Subnets { 1865 subnetsMap[subnet.CIDR] = space.ProviderId 1866 } 1867 } 1868 return subnetsMap, nil 1869 } 1870 1871 // Spaces returns all the spaces, that have subnets, known to the provider. 1872 // Space name is not filled in as the provider doesn't know the juju name for 1873 // the space. 1874 func (environ *maasEnviron) Spaces() ([]network.SpaceInfo, error) { 1875 if !environ.usingMAAS2() { 1876 return environ.spaces1() 1877 } 1878 return environ.spaces2() 1879 } 1880 1881 func (environ *maasEnviron) spaces1() ([]network.SpaceInfo, error) { 1882 spacesClient := environ.getMAASClient().GetSubObject("spaces") 1883 spacesJson, err := spacesClient.CallGet("", nil) 1884 if err != nil { 1885 return nil, errors.Trace(err) 1886 } 1887 spacesArray, err := spacesJson.GetArray() 1888 if err != nil { 1889 return nil, errors.Trace(err) 1890 } 1891 spaces := []network.SpaceInfo{} 1892 for _, spaceJson := range spacesArray { 1893 spaceMap, err := spaceJson.GetMap() 1894 if err != nil { 1895 return nil, errors.Trace(err) 1896 } 1897 providerIdRaw, err := spaceMap["id"].GetFloat64() 1898 if err != nil { 1899 return nil, errors.Trace(err) 1900 } 1901 providerId := network.Id(fmt.Sprintf("%.0f", providerIdRaw)) 1902 name, err := spaceMap["name"].GetString() 1903 if err != nil { 1904 return nil, errors.Trace(err) 1905 } 1906 1907 space := network.SpaceInfo{Name: name, ProviderId: providerId} 1908 subnetsArray, err := spaceMap["subnets"].GetArray() 1909 if err != nil { 1910 return nil, errors.Trace(err) 1911 } 1912 for _, subnetJson := range subnetsArray { 1913 subnet, err := environ.subnetFromJson(subnetJson, providerId) 1914 if err != nil { 1915 return nil, errors.Trace(err) 1916 } 1917 space.Subnets = append(space.Subnets, subnet) 1918 } 1919 // Skip spaces with no subnets. 1920 if len(space.Subnets) > 0 { 1921 spaces = append(spaces, space) 1922 } 1923 } 1924 return spaces, nil 1925 } 1926 1927 func (environ *maasEnviron) spaces2() ([]network.SpaceInfo, error) { 1928 spaces, err := environ.maasController.Spaces() 1929 if err != nil { 1930 return nil, errors.Trace(err) 1931 } 1932 var result []network.SpaceInfo 1933 for _, space := range spaces { 1934 if len(space.Subnets()) == 0 { 1935 continue 1936 } 1937 outSpace := network.SpaceInfo{ 1938 Name: space.Name(), 1939 ProviderId: network.Id(strconv.Itoa(space.ID())), 1940 Subnets: make([]network.SubnetInfo, len(space.Subnets())), 1941 } 1942 for i, subnet := range space.Subnets() { 1943 subnetInfo := network.SubnetInfo{ 1944 ProviderId: network.Id(strconv.Itoa(subnet.ID())), 1945 VLANTag: subnet.VLAN().VID(), 1946 CIDR: subnet.CIDR(), 1947 SpaceProviderId: network.Id(strconv.Itoa(space.ID())), 1948 } 1949 outSpace.Subnets[i] = subnetInfo 1950 } 1951 result = append(result, outSpace) 1952 } 1953 return result, nil 1954 } 1955 1956 // Subnets returns basic information about the specified subnets known 1957 // by the provider for the specified instance. subnetIds must not be 1958 // empty. Implements NetworkingEnviron.Subnets. 1959 func (environ *maasEnviron) Subnets(instId instance.Id, subnetIds []network.Id) ([]network.SubnetInfo, error) { 1960 if environ.usingMAAS2() { 1961 return environ.subnets2(instId, subnetIds) 1962 } 1963 return environ.subnets1(instId, subnetIds) 1964 } 1965 1966 func (environ *maasEnviron) subnets1(instId instance.Id, subnetIds []network.Id) ([]network.SubnetInfo, error) { 1967 var nodeId string 1968 if instId != instance.UnknownId { 1969 inst, err := environ.getInstance(instId) 1970 if err != nil { 1971 return nil, errors.Trace(err) 1972 } 1973 nodeId, err = environ.nodeIdFromInstance(inst) 1974 if err != nil { 1975 return nil, errors.Trace(err) 1976 } 1977 } 1978 subnets, err := environ.filteredSubnets(nodeId, subnetIds) 1979 if err != nil { 1980 return nil, errors.Trace(err) 1981 } 1982 if instId != instance.UnknownId { 1983 logger.Debugf("instance %q has subnets %v", instId, subnets) 1984 } else { 1985 logger.Debugf("found subnets %v", subnets) 1986 } 1987 1988 return subnets, nil 1989 } 1990 1991 func (environ *maasEnviron) subnets2(instId instance.Id, subnetIds []network.Id) ([]network.SubnetInfo, error) { 1992 subnets := []network.SubnetInfo{} 1993 if instId == instance.UnknownId { 1994 spaces, err := environ.Spaces() 1995 if err != nil { 1996 return nil, errors.Trace(err) 1997 } 1998 for _, space := range spaces { 1999 subnets = append(subnets, space.Subnets...) 2000 } 2001 } else { 2002 var err error 2003 subnets, err = environ.filteredSubnets2(instId) 2004 if err != nil { 2005 return nil, errors.Trace(err) 2006 } 2007 } 2008 2009 if len(subnetIds) == 0 { 2010 return subnets, nil 2011 } 2012 result := []network.SubnetInfo{} 2013 subnetMap := make(map[string]bool) 2014 for _, subnetId := range subnetIds { 2015 subnetMap[string(subnetId)] = false 2016 } 2017 for _, subnet := range subnets { 2018 _, ok := subnetMap[string(subnet.ProviderId)] 2019 if !ok { 2020 // This id is not what we're looking for. 2021 continue 2022 } 2023 subnetMap[string(subnet.ProviderId)] = true 2024 result = append(result, subnet) 2025 } 2026 2027 return result, checkNotFound(subnetMap) 2028 } 2029 2030 func (environ *maasEnviron) filteredSubnets2(instId instance.Id) ([]network.SubnetInfo, error) { 2031 args := gomaasapi.MachinesArgs{ 2032 AgentName: environ.ecfg().maasAgentName(), 2033 SystemIDs: []string{string(instId)}, 2034 } 2035 machines, err := environ.maasController.Machines(args) 2036 if err != nil { 2037 return nil, errors.Trace(err) 2038 } 2039 if len(machines) == 0 { 2040 return nil, errors.NotFoundf("machine %v", instId) 2041 } else if len(machines) > 1 { 2042 return nil, errors.Errorf("unexpected response getting machine details %v: %v", instId, machines) 2043 } 2044 2045 machine := machines[0] 2046 spaceMap, err := environ.buildSpaceMap() 2047 if err != nil { 2048 return nil, errors.Trace(err) 2049 } 2050 result := []network.SubnetInfo{} 2051 for _, iface := range machine.InterfaceSet() { 2052 for _, link := range iface.Links() { 2053 subnet := link.Subnet() 2054 space, ok := spaceMap[subnet.Space()] 2055 if !ok { 2056 return nil, errors.Errorf("missing space %v on subnet %v", subnet.Space(), subnet.CIDR()) 2057 } 2058 subnetInfo := network.SubnetInfo{ 2059 ProviderId: network.Id(strconv.Itoa(subnet.ID())), 2060 VLANTag: subnet.VLAN().VID(), 2061 CIDR: subnet.CIDR(), 2062 SpaceProviderId: space.ProviderId, 2063 } 2064 result = append(result, subnetInfo) 2065 } 2066 } 2067 return result, nil 2068 } 2069 2070 func checkNotFound(subnetIdSet map[string]bool) error { 2071 notFound := []string{} 2072 for subnetId, found := range subnetIdSet { 2073 if !found { 2074 notFound = append(notFound, string(subnetId)) 2075 } 2076 } 2077 if len(notFound) != 0 { 2078 return errors.Errorf("failed to find the following subnets: %v", strings.Join(notFound, ", ")) 2079 } 2080 return nil 2081 } 2082 2083 // AllInstances returns all the instance.Instance in this provider. 2084 func (environ *maasEnviron) AllInstances() ([]instance.Instance, error) { 2085 return environ.acquiredInstances(nil) 2086 } 2087 2088 // Storage is defined by the Environ interface. 2089 func (env *maasEnviron) Storage() storage.Storage { 2090 env.ecfgMutex.Lock() 2091 defer env.ecfgMutex.Unlock() 2092 return env.storageUnlocked 2093 } 2094 2095 func (environ *maasEnviron) Destroy() error { 2096 if environ.ecfg().maasAgentName() == "" { 2097 logger.Warningf("No MAAS agent name specified.\n\n" + 2098 "The environment is either not running or from a very early Juju version.\n" + 2099 "It is not safe to release all MAAS instances without an agent name.\n" + 2100 "If the environment is still running, please manually decomission the MAAS machines.") 2101 return errors.New("unsafe destruction") 2102 } 2103 if err := common.Destroy(environ); err != nil { 2104 return errors.Trace(err) 2105 } 2106 return environ.Storage().RemoveAll() 2107 } 2108 2109 // MAAS does not do firewalling so these port methods do nothing. 2110 func (*maasEnviron) OpenPorts([]network.PortRange) error { 2111 logger.Debugf("unimplemented OpenPorts() called") 2112 return nil 2113 } 2114 2115 func (*maasEnviron) ClosePorts([]network.PortRange) error { 2116 logger.Debugf("unimplemented ClosePorts() called") 2117 return nil 2118 } 2119 2120 func (*maasEnviron) Ports() ([]network.PortRange, error) { 2121 logger.Debugf("unimplemented Ports() called") 2122 return nil, nil 2123 } 2124 2125 func (*maasEnviron) Provider() environs.EnvironProvider { 2126 return &providerInstance 2127 } 2128 2129 func (environ *maasEnviron) nodeIdFromInstance(inst instance.Instance) (string, error) { 2130 maasInst := inst.(*maas1Instance) 2131 maasObj := maasInst.maasObject 2132 nodeId, err := maasObj.GetField("system_id") 2133 if err != nil { 2134 return "", err 2135 } 2136 return nodeId, err 2137 } 2138 2139 func (env *maasEnviron) AllocateContainerAddresses(hostInstanceID instance.Id, preparedInfo []network.InterfaceInfo) ([]network.InterfaceInfo, error) { 2140 if len(preparedInfo) == 0 { 2141 return nil, errors.Errorf("no prepared info to allocate") 2142 } 2143 logger.Debugf("using prepared container info: %+v", preparedInfo) 2144 if !env.usingMAAS2() { 2145 return env.allocateContainerAddresses1(hostInstanceID, preparedInfo) 2146 } 2147 return env.allocateContainerAddresses2(hostInstanceID, preparedInfo) 2148 } 2149 2150 func (env *maasEnviron) allocateContainerAddresses1(hostInstanceID instance.Id, preparedInfo []network.InterfaceInfo) ([]network.InterfaceInfo, error) { 2151 subnetCIDRToVLANID := make(map[string]string) 2152 subnetsAPI := env.getMAASClient().GetSubObject("subnets") 2153 result, err := subnetsAPI.CallGet("", nil) 2154 if err != nil { 2155 return nil, errors.Annotate(err, "cannot get subnets") 2156 } 2157 subnetsJSON, err := getJSONBytes(result) 2158 if err != nil { 2159 return nil, errors.Annotate(err, "cannot get subnets JSON") 2160 } 2161 var subnets []maasSubnet 2162 if err := json.Unmarshal(subnetsJSON, &subnets); err != nil { 2163 return nil, errors.Annotate(err, "cannot parse subnets JSON") 2164 } 2165 for _, subnet := range subnets { 2166 subnetCIDRToVLANID[subnet.CIDR] = strconv.Itoa(subnet.VLAN.ID) 2167 } 2168 2169 var primaryNICInfo network.InterfaceInfo 2170 for _, nic := range preparedInfo { 2171 if nic.InterfaceName == "eth0" { 2172 primaryNICInfo = nic 2173 break 2174 } 2175 } 2176 if primaryNICInfo.InterfaceName == "" { 2177 return nil, errors.Errorf("cannot find primary interface for container") 2178 } 2179 logger.Debugf("primary device NIC prepared info: %+v", primaryNICInfo) 2180 2181 primaryMACAddress := primaryNICInfo.MACAddress 2182 containerDevice, err := env.createDevice(hostInstanceID, primaryMACAddress) 2183 if err != nil { 2184 return nil, errors.Annotate(err, "cannot create device for container") 2185 } 2186 deviceID := instance.Id(containerDevice.ResourceURI) 2187 logger.Debugf("created device %q with primary MAC address %q", deviceID, primaryMACAddress) 2188 2189 interfaces, err := env.deviceInterfaces(deviceID) 2190 if err != nil { 2191 return nil, errors.Annotate(err, "cannot get device interfaces") 2192 } 2193 if len(interfaces) != 1 { 2194 return nil, errors.Errorf("expected 1 device interface, got %d", len(interfaces)) 2195 } 2196 2197 primaryNICName := interfaces[0].Name 2198 primaryNICID := strconv.Itoa(interfaces[0].ID) 2199 primaryNICSubnetCIDR := primaryNICInfo.CIDR 2200 primaryNICVLANID := subnetCIDRToVLANID[primaryNICSubnetCIDR] 2201 updatedPrimaryNIC, err := env.updateDeviceInterface(deviceID, primaryNICID, primaryNICName, primaryMACAddress, primaryNICVLANID) 2202 if err != nil { 2203 return nil, errors.Annotatef(err, "cannot update device interface %q", interfaces[0].Name) 2204 } 2205 logger.Debugf("device %q primary interface %q updated: %+v", containerDevice.SystemID, primaryNICName, updatedPrimaryNIC) 2206 2207 deviceNICIDs := make([]string, len(preparedInfo)) 2208 nameToParentName := make(map[string]string) 2209 for i, nic := range preparedInfo { 2210 maasNICID := "" 2211 nameToParentName[nic.InterfaceName] = nic.ParentInterfaceName 2212 if nic.InterfaceName != primaryNICName { 2213 nicVLANID := subnetCIDRToVLANID[nic.CIDR] 2214 createdNIC, err := env.createDeviceInterface(deviceID, nic.InterfaceName, nic.MACAddress, nicVLANID) 2215 if err != nil { 2216 return nil, errors.Annotate(err, "creating device interface") 2217 } 2218 maasNICID = strconv.Itoa(createdNIC.ID) 2219 logger.Debugf("created device interface: %+v", createdNIC) 2220 } else { 2221 maasNICID = primaryNICID 2222 } 2223 deviceNICIDs[i] = maasNICID 2224 subnetID := string(nic.ProviderSubnetId) 2225 2226 linkedInterface, err := env.linkDeviceInterfaceToSubnet(deviceID, maasNICID, subnetID, modeStatic) 2227 if err != nil { 2228 return nil, errors.Annotate(err, "cannot link device interface to subnet") 2229 } 2230 logger.Debugf("linked device interface to subnet: %+v", linkedInterface) 2231 } 2232 finalInterfaces, err := env.deviceInterfaceInfo(deviceID, nameToParentName) 2233 if err != nil { 2234 return nil, errors.Annotate(err, "cannot get device interfaces") 2235 } 2236 logger.Debugf("allocated device interfaces: %+v", finalInterfaces) 2237 return finalInterfaces, nil 2238 } 2239 2240 func (env *maasEnviron) allocateContainerAddresses2(hostInstanceID instance.Id, preparedInfo []network.InterfaceInfo) ([]network.InterfaceInfo, error) { 2241 subnetCIDRToSubnet := make(map[string]gomaasapi.Subnet) 2242 spaces, err := env.maasController.Spaces() 2243 if err != nil { 2244 return nil, errors.Trace(err) 2245 } 2246 for _, space := range spaces { 2247 for _, subnet := range space.Subnets() { 2248 subnetCIDRToSubnet[subnet.CIDR()] = subnet 2249 } 2250 } 2251 2252 var primaryNICInfo network.InterfaceInfo 2253 primaryNICName := "eth0" 2254 for _, nic := range preparedInfo { 2255 if nic.InterfaceName == primaryNICName { 2256 primaryNICInfo = nic 2257 break 2258 } 2259 } 2260 if primaryNICInfo.InterfaceName == "" { 2261 return nil, errors.Errorf("cannot find primary interface for container") 2262 } 2263 logger.Debugf("primary device NIC prepared info: %+v", primaryNICInfo) 2264 2265 primaryNICSubnetCIDR := primaryNICInfo.CIDR 2266 subnet, ok := subnetCIDRToSubnet[primaryNICSubnetCIDR] 2267 if !ok { 2268 return nil, errors.Errorf("primary NIC subnet %v not found", primaryNICSubnetCIDR) 2269 } 2270 primaryMACAddress := primaryNICInfo.MACAddress 2271 args := gomaasapi.MachinesArgs{ 2272 AgentName: env.ecfg().maasAgentName(), 2273 SystemIDs: []string{string(hostInstanceID)}, 2274 } 2275 machines, err := env.maasController.Machines(args) 2276 if err != nil { 2277 return nil, errors.Trace(err) 2278 } 2279 if len(machines) != 1 { 2280 return nil, errors.Errorf("unexpected response fetching machine %v: %v", hostInstanceID, machines) 2281 } 2282 machine := machines[0] 2283 createDeviceArgs := gomaasapi.CreateMachineDeviceArgs{ 2284 MACAddress: primaryMACAddress, 2285 Subnet: subnet, 2286 InterfaceName: primaryNICName, 2287 } 2288 device, err := machine.CreateDevice(createDeviceArgs) 2289 if err != nil { 2290 return nil, errors.Trace(err) 2291 } 2292 interface_set := device.InterfaceSet() 2293 if len(interface_set) != 1 { 2294 // Shouldn't be possible as machine.CreateDevice always returns us 2295 // one interface. 2296 return nil, errors.Errorf("unexpected number of interfaces inresponse from creating device: %v", interface_set) 2297 } 2298 2299 nameToParentName := make(map[string]string) 2300 for _, nic := range preparedInfo { 2301 nameToParentName[nic.InterfaceName] = nic.ParentInterfaceName 2302 if nic.InterfaceName != primaryNICName { 2303 subnet, ok := subnetCIDRToSubnet[nic.CIDR] 2304 if !ok { 2305 return nil, errors.Errorf("NIC %v subnet %v not found", nic.InterfaceName, nic.CIDR) 2306 } 2307 createdNIC, err := device.CreateInterface( 2308 gomaasapi.CreateInterfaceArgs{ 2309 Name: nic.InterfaceName, 2310 MACAddress: nic.MACAddress, 2311 VLAN: subnet.VLAN(), 2312 }) 2313 if err != nil { 2314 return nil, errors.Annotate(err, "creating device interface") 2315 } 2316 logger.Debugf("created device interface: %+v", createdNIC) 2317 2318 linkArgs := gomaasapi.LinkSubnetArgs{ 2319 Mode: gomaasapi.LinkModeStatic, 2320 Subnet: subnet, 2321 } 2322 err = createdNIC.LinkSubnet(linkArgs) 2323 if err != nil { 2324 return nil, errors.Annotate(err, "cannot link device interface to subnet") 2325 } 2326 logger.Debugf("linked device interface to subnet: %+v", createdNIC) 2327 } 2328 } 2329 finalInterfaces, err := env.deviceInterfaceInfo2(device.SystemID(), nameToParentName) 2330 if err != nil { 2331 return nil, errors.Annotate(err, "cannot get device interfaces") 2332 } 2333 logger.Debugf("allocated device interfaces: %+v", finalInterfaces) 2334 return finalInterfaces, nil 2335 }