github.com/juju/juju@v0.0.0-20240430160146-1752b71fcf00/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 stdcontext "context" 8 "fmt" 9 "net/http" 10 "regexp" 11 "strconv" 12 "strings" 13 "sync" 14 "time" 15 16 "github.com/juju/clock" 17 "github.com/juju/collections/set" 18 "github.com/juju/errors" 19 "github.com/juju/gomaasapi/v2" 20 "github.com/juju/names/v5" 21 "github.com/juju/retry" 22 "github.com/juju/version/v2" 23 24 "github.com/juju/juju/cloudconfig/cloudinit" 25 "github.com/juju/juju/cloudconfig/instancecfg" 26 "github.com/juju/juju/cloudconfig/providerinit" 27 corebase "github.com/juju/juju/core/base" 28 "github.com/juju/juju/core/constraints" 29 "github.com/juju/juju/core/instance" 30 corenetwork "github.com/juju/juju/core/network" 31 "github.com/juju/juju/core/os/ostype" 32 "github.com/juju/juju/core/status" 33 "github.com/juju/juju/environs" 34 environscloudspec "github.com/juju/juju/environs/cloudspec" 35 "github.com/juju/juju/environs/config" 36 "github.com/juju/juju/environs/context" 37 "github.com/juju/juju/environs/instances" 38 "github.com/juju/juju/environs/storage" 39 "github.com/juju/juju/environs/tags" 40 "github.com/juju/juju/provider/common" 41 "github.com/juju/juju/tools" 42 ) 43 44 const ( 45 // The version strings indicating the MAAS API version. 46 apiVersion2 = "2.0" 47 ) 48 49 var defaultShortRetryStrategy = retry.CallArgs{ 50 Clock: clock.WallClock, 51 Delay: 200 * time.Millisecond, 52 MaxDuration: 5 * time.Second, 53 } 54 var defaultLongRetryStrategy = retry.CallArgs{ 55 Clock: clock.WallClock, 56 Delay: 10 * time.Second, 57 MaxDuration: 1200 * time.Second, 58 } 59 60 var ( 61 DeploymentStatusCall = deploymentStatusCall 62 GetMAASController = getMAASController 63 ) 64 65 func getMAASController(maasServer, apiKey string) (gomaasapi.Controller, error) { 66 return gomaasapi.NewController(gomaasapi.ControllerArgs{ 67 BaseURL: maasServer, 68 APIKey: apiKey, 69 }) 70 } 71 72 type maasEnviron struct { 73 name string 74 uuid string 75 76 // archMutex gates access to supportedArchitectures 77 archMutex sync.Mutex 78 79 // ecfgMutex protects the *Unlocked fields below. 80 ecfgMutex sync.Mutex 81 82 ecfgUnlocked *maasModelConfig 83 storageUnlocked storage.Storage 84 85 // maasController provides access to the MAAS 2.0 API. 86 maasController gomaasapi.Controller 87 88 // namespace is used to create the machine and device hostnames. 89 namespace instance.Namespace 90 91 // apiVersion tells us if we are using the MAAS 1.0 or 2.0 api. 92 apiVersion string 93 94 // GetCapabilities is a function that connects to MAAS to return its set of 95 // capabilities. 96 GetCapabilities Capabilities 97 98 // A request may fail to due "eventual consistency" semantics, which 99 // should resolve fairly quickly. A request may also fail due to a slow 100 // state transition (for instance an instance taking a while to release 101 // a security group after termination). The former failure mode is 102 // dealt with by shortRetryStrategy, the latter by longRetryStrategy 103 shortRetryStrategy retry.CallArgs 104 longRetryStrategy retry.CallArgs 105 } 106 107 var _ environs.Environ = (*maasEnviron)(nil) 108 var _ environs.Networking = (*maasEnviron)(nil) 109 110 // Capabilities is an alias for a function that gets 111 // the capabilities of a MAAS installation. 112 type Capabilities = func(client *gomaasapi.MAASObject, serverURL string) (set.Strings, error) 113 114 func NewEnviron(cloud environscloudspec.CloudSpec, cfg *config.Config, getCaps Capabilities) (*maasEnviron, error) { 115 if getCaps == nil { 116 getCaps = getCapabilities 117 } 118 env := &maasEnviron{ 119 name: cfg.Name(), 120 uuid: cfg.UUID(), 121 GetCapabilities: getCaps, 122 shortRetryStrategy: defaultShortRetryStrategy, 123 longRetryStrategy: defaultLongRetryStrategy, 124 } 125 if err := env.SetConfig(cfg); err != nil { 126 return nil, errors.Trace(err) 127 } 128 if err := env.SetCloudSpec(stdcontext.TODO(), cloud); err != nil { 129 return nil, errors.Trace(err) 130 } 131 132 var err error 133 env.namespace, err = instance.NewNamespace(cfg.UUID()) 134 if err != nil { 135 return nil, errors.Trace(err) 136 } 137 return env, nil 138 } 139 140 // PrepareForBootstrap is part of the Environ interface. 141 func (env *maasEnviron) PrepareForBootstrap(_ environs.BootstrapContext, _ string) error { 142 return nil 143 } 144 145 // Create is part of the Environ interface. 146 func (env *maasEnviron) Create(_ context.ProviderCallContext, _ environs.CreateParams) error { 147 return nil 148 } 149 150 // Bootstrap is part of the Environ interface. 151 func (env *maasEnviron) Bootstrap( 152 ctx environs.BootstrapContext, callCtx context.ProviderCallContext, args environs.BootstrapParams, 153 ) (*environs.BootstrapResult, error) { 154 result, base, finalizer, err := common.BootstrapInstance(ctx, env, callCtx, args) 155 if err != nil { 156 return nil, err 157 } 158 159 // We want to destroy the started instance if it doesn't transition to Deployed. 160 defer func() { 161 if err != nil { 162 if err := env.StopInstances(callCtx, result.Instance.Id()); err != nil { 163 logger.Errorf("error releasing bootstrap instance: %v", err) 164 } 165 } 166 }() 167 168 waitingFinalizer := func( 169 ctx environs.BootstrapContext, 170 icfg *instancecfg.InstanceConfig, 171 dialOpts environs.BootstrapDialOpts, 172 ) error { 173 // Wait for bootstrap instance to change to deployed state. 174 if err := env.waitForNodeDeployment(callCtx, result.Instance.Id(), dialOpts.Timeout); err != nil { 175 return errors.Annotate(err, "bootstrap instance started but did not change to Deployed state") 176 } 177 return finalizer(ctx, icfg, dialOpts) 178 } 179 180 bsResult := &environs.BootstrapResult{ 181 Arch: *result.Hardware.Arch, 182 Base: *base, 183 CloudBootstrapFinalizer: waitingFinalizer, 184 } 185 return bsResult, nil 186 } 187 188 // ControllerInstances is specified in the Environ interface. 189 func (env *maasEnviron) ControllerInstances(ctx context.ProviderCallContext, controllerUUID string) ([]instance.Id, error) { 190 instances, err := env.instances(ctx, gomaasapi.MachinesArgs{ 191 OwnerData: map[string]string{ 192 tags.JujuIsController: "true", 193 tags.JujuController: controllerUUID, 194 }, 195 }) 196 if err != nil { 197 return nil, errors.Trace(err) 198 } 199 if len(instances) == 0 { 200 return nil, environs.ErrNotBootstrapped 201 } 202 ids := make([]instance.Id, len(instances)) 203 for i := range instances { 204 ids[i] = instances[i].Id() 205 } 206 return ids, nil 207 } 208 209 // ecfg returns the environment's maasModelConfig, and protects it with a 210 // mutex. 211 func (env *maasEnviron) ecfg() *maasModelConfig { 212 env.ecfgMutex.Lock() 213 cfg := *env.ecfgUnlocked 214 env.ecfgMutex.Unlock() 215 return &cfg 216 } 217 218 // Config is specified in the Environ interface. 219 func (env *maasEnviron) Config() *config.Config { 220 return env.ecfg().Config 221 } 222 223 // SetConfig is specified in the Environ interface. 224 func (env *maasEnviron) SetConfig(cfg *config.Config) error { 225 env.ecfgMutex.Lock() 226 defer env.ecfgMutex.Unlock() 227 228 // The new config has already been validated by itself, but now we 229 // validate the transition from the old config to the new. 230 var oldCfg *config.Config 231 if env.ecfgUnlocked != nil { 232 oldCfg = env.ecfgUnlocked.Config 233 } 234 cfg, err := env.Provider().Validate(cfg, oldCfg) 235 if err != nil { 236 return errors.Trace(err) 237 } 238 239 ecfg, err := providerInstance.newConfig(cfg) 240 if err != nil { 241 return errors.Trace(err) 242 } 243 244 env.ecfgUnlocked = ecfg 245 246 return nil 247 } 248 249 // SetCloudSpec is specified in the environs.Environ interface. 250 func (env *maasEnviron) SetCloudSpec(_ stdcontext.Context, spec environscloudspec.CloudSpec) error { 251 env.ecfgMutex.Lock() 252 defer env.ecfgMutex.Unlock() 253 254 maasServer, err := parseCloudEndpoint(spec.Endpoint) 255 if err != nil { 256 return errors.Trace(err) 257 } 258 maasOAuth, err := parseOAuthToken(*spec.Credential) 259 if err != nil { 260 return errors.Trace(err) 261 } 262 263 apiVersion := apiVersion2 264 controller, err := GetMAASController(maasServer, maasOAuth) 265 if err != nil { 266 return errors.Trace(err) 267 } 268 269 env.maasController = controller 270 env.apiVersion = apiVersion 271 env.storageUnlocked = NewStorage(env) 272 273 return nil 274 } 275 276 // ValidateCloudEndpoint returns nil if the current model can talk to the maas 277 // endpoint. Used as validation during model upgrades. 278 // Implements environs.CloudEndpointChecker 279 func (env *maasEnviron) ValidateCloudEndpoint(ctx context.ProviderCallContext) error { 280 _, _, err := env.maasController.APIVersionInfo() 281 return errors.Trace(err) 282 } 283 284 func (env *maasEnviron) getSupportedArchitectures(ctx context.ProviderCallContext) ([]string, error) { 285 env.archMutex.Lock() 286 defer env.archMutex.Unlock() 287 288 resources, err := env.maasController.BootResources() 289 if err != nil { 290 common.HandleCredentialError(IsAuthorisationFailure, err, ctx) 291 return nil, errors.Trace(err) 292 } 293 architectures := set.NewStrings() 294 for _, resource := range resources { 295 architectures.Add(strings.Split(resource.Architecture(), "/")[0]) 296 } 297 return architectures.SortedValues(), nil 298 } 299 300 // SupportsSpaces is specified on environs.Networking. 301 func (env *maasEnviron) SupportsSpaces(ctx context.ProviderCallContext) (bool, error) { 302 return true, nil 303 } 304 305 // SupportsSpaceDiscovery is specified on environs.Networking. 306 func (env *maasEnviron) SupportsSpaceDiscovery(ctx context.ProviderCallContext) (bool, error) { 307 return true, nil 308 } 309 310 // SupportsContainerAddresses is specified on environs.Networking. 311 func (env *maasEnviron) SupportsContainerAddresses(ctx context.ProviderCallContext) (bool, error) { 312 return true, nil 313 } 314 315 type maasAvailabilityZone struct { 316 name string 317 } 318 319 func (z maasAvailabilityZone) Name() string { 320 return z.name 321 } 322 323 func (z maasAvailabilityZone) Available() bool { 324 // MAAS' physical zone attributes only include name and description; 325 // there is no concept of availability. 326 return true 327 } 328 329 // AvailabilityZones returns a slice of availability zones 330 // for the configured region. 331 func (env *maasEnviron) AvailabilityZones(ctx context.ProviderCallContext) (corenetwork.AvailabilityZones, error) { 332 zones, err := env.availabilityZones(ctx) 333 return zones, errors.Trace(err) 334 } 335 336 func (env *maasEnviron) availabilityZones(ctx context.ProviderCallContext) (corenetwork.AvailabilityZones, error) { 337 zones, err := env.maasController.Zones() 338 if err != nil { 339 common.HandleCredentialError(IsAuthorisationFailure, err, ctx) 340 return nil, errors.Trace(err) 341 } 342 availabilityZones := make(corenetwork.AvailabilityZones, len(zones)) 343 for i, zone := range zones { 344 availabilityZones[i] = maasAvailabilityZone{zone.Name()} 345 } 346 return availabilityZones, nil 347 } 348 349 // InstanceAvailabilityZoneNames returns the availability zone names for each 350 // of the specified instances. 351 func (env *maasEnviron) InstanceAvailabilityZoneNames(ctx context.ProviderCallContext, ids []instance.Id) (map[instance.Id]string, error) { 352 inst, err := env.Instances(ctx, ids) 353 if err != nil && err != environs.ErrPartialInstances { 354 return nil, err 355 } 356 zones := make(map[instance.Id]string, 0) 357 for _, inst := range inst { 358 if inst == nil { 359 continue 360 } 361 mInst, ok := inst.(*maasInstance) 362 if !ok { 363 continue 364 } 365 z, err := mInst.zone() 366 if err != nil { 367 logger.Errorf("could not get availability zone %v", err) 368 continue 369 } 370 zones[inst.Id()] = z 371 } 372 return zones, nil 373 } 374 375 // DeriveAvailabilityZones is part of the common.ZonedEnviron interface. 376 func (env *maasEnviron) DeriveAvailabilityZones(ctx context.ProviderCallContext, args environs.StartInstanceParams) ([]string, error) { 377 if args.Placement != "" { 378 placement, err := env.parsePlacement(ctx, args.Placement) 379 if err != nil { 380 return nil, errors.Trace(err) 381 } 382 if placement.zoneName != "" { 383 return []string{placement.zoneName}, nil 384 } 385 } 386 return nil, nil 387 } 388 389 type maasPlacement struct { 390 nodeName string 391 zoneName string 392 systemId string 393 } 394 395 func (env *maasEnviron) parsePlacement(ctx context.ProviderCallContext, placement string) (*maasPlacement, error) { 396 pos := strings.IndexRune(placement, '=') 397 if pos == -1 { 398 // If there's no '=' delimiter, assume it's a node name. 399 return &maasPlacement{nodeName: placement}, nil 400 } 401 switch key, value := placement[:pos], placement[pos+1:]; key { 402 case "zone": 403 zones, err := env.AvailabilityZones(ctx) 404 if err != nil { 405 return nil, errors.Trace(err) 406 } 407 if err := zones.Validate(value); err != nil { 408 return nil, errors.Trace(err) 409 } 410 411 return &maasPlacement{zoneName: value}, nil 412 case "system-id": 413 return &maasPlacement{systemId: value}, nil 414 } 415 416 return nil, errors.Errorf("unknown placement directive: %v", placement) 417 } 418 419 func (env *maasEnviron) PrecheckInstance(ctx context.ProviderCallContext, args environs.PrecheckInstanceParams) error { 420 if args.Placement == "" { 421 return nil 422 } 423 _, err := env.parsePlacement(ctx, args.Placement) 424 return err 425 } 426 427 // getCapabilities asks the MAAS server for its capabilities, if 428 // supported by the server. 429 func getCapabilities(client *gomaasapi.MAASObject, serverURL string) (set.Strings, error) { 430 caps := make(set.Strings) 431 var result gomaasapi.JSONObject 432 433 retryStrategy := defaultShortRetryStrategy 434 retryStrategy.IsFatalError = func(err error) bool { 435 if err, ok := errors.Cause(err).(gomaasapi.ServerError); ok && err.StatusCode == 404 { 436 return true 437 } 438 return false 439 } 440 retryStrategy.Func = func() error { 441 var err error 442 version := client.GetSubObject("version/") 443 result, err = version.CallGet("", nil) 444 return err 445 } 446 err := retry.Call(retryStrategy) 447 448 if retry.IsAttemptsExceeded(err) || retry.IsDurationExceeded(err) { 449 logger.Debugf("Can't connect to maas server at endpoint %q: %v", serverURL, err) 450 err = retry.LastError(err) 451 return caps, err 452 } 453 if err != nil { 454 err, _ := errors.Cause(err).(gomaasapi.ServerError) 455 logger.Debugf("Failed attempting to get capabilities from maas endpoint %q: %v", serverURL, err) 456 457 message := "could not connect to MAAS controller - check the endpoint is correct" 458 trimmedURL := strings.TrimRight(serverURL, "/") 459 if !strings.HasSuffix(trimmedURL, "/MAAS") { 460 message += " (it normally ends with /MAAS)" 461 } 462 return caps, errors.NewNotSupported(nil, message) 463 } 464 465 info, err := result.GetMap() 466 if err != nil { 467 logger.Debugf("Invalid data returned from maas endpoint %q: %v", serverURL, err) 468 // invalid data of some sort, probably not a MAAS server. 469 return caps, errors.New("failed to get expected data from server") 470 } 471 capsObj, ok := info["capabilities"] 472 if !ok { 473 return caps, fmt.Errorf("MAAS does not report capabilities") 474 } 475 items, err := capsObj.GetArray() 476 if err != nil { 477 logger.Debugf("Invalid data returned from maas endpoint %q: %v", serverURL, err) 478 return caps, errors.New("failed to get expected data from server") 479 } 480 for _, item := range items { 481 val, err := item.GetString() 482 if err != nil { 483 logger.Debugf("Invalid data returned from maas endpoint %q: %v", serverURL, err) 484 return set.NewStrings(), errors.New("failed to get expected data from server") 485 } 486 caps.Add(val) 487 } 488 return caps, nil 489 } 490 491 var dashSuffix = regexp.MustCompile("^(.*)-\\d+$") 492 493 func spaceNamesToSpaceInfo( 494 spaces []string, spaceMap map[string]corenetwork.SpaceInfo, 495 ) ([]corenetwork.SpaceInfo, error) { 496 var spaceInfos []corenetwork.SpaceInfo 497 for _, name := range spaces { 498 info, ok := spaceMap[name] 499 if !ok { 500 matches := dashSuffix.FindAllStringSubmatch(name, 1) 501 if matches == nil { 502 return nil, errors.Errorf("unrecognised space in constraint %q", name) 503 } 504 // A -number was added to the space name when we 505 // converted to a juju name, we found 506 info, ok = spaceMap[matches[0][1]] 507 if !ok { 508 return nil, errors.Errorf("unrecognised space in constraint %q", name) 509 } 510 } 511 spaceInfos = append(spaceInfos, info) 512 } 513 return spaceInfos, nil 514 } 515 516 func (env *maasEnviron) buildSpaceMap(ctx context.ProviderCallContext) (map[string]corenetwork.SpaceInfo, error) { 517 spaces, err := env.Spaces(ctx) 518 if err != nil { 519 return nil, errors.Trace(err) 520 } 521 spaceMap := make(map[string]corenetwork.SpaceInfo) 522 empty := set.Strings{} 523 for _, space := range spaces { 524 jujuName := corenetwork.ConvertSpaceName(string(space.Name), empty) 525 spaceMap[jujuName] = space 526 } 527 return spaceMap, nil 528 } 529 530 func (env *maasEnviron) spaceNamesToSpaceInfo( 531 ctx context.ProviderCallContext, positiveSpaces, negativeSpaces []string, 532 ) ([]corenetwork.SpaceInfo, []corenetwork.SpaceInfo, error) { 533 spaceMap, err := env.buildSpaceMap(ctx) 534 if err != nil { 535 return nil, nil, errors.Trace(err) 536 } 537 538 positiveSpaceIds, err := spaceNamesToSpaceInfo(positiveSpaces, spaceMap) 539 if err != nil { 540 return nil, nil, errors.Trace(err) 541 } 542 negativeSpaceIds, err := spaceNamesToSpaceInfo(negativeSpaces, spaceMap) 543 if err != nil { 544 return nil, nil, errors.Trace(err) 545 } 546 return positiveSpaceIds, negativeSpaceIds, nil 547 } 548 549 // networkSpaceRequirements combines the space requirements for the application 550 // bindings and the specified constraints and returns a set of provider 551 // space IDs for which a NIC needs to be provisioned in the instance we are 552 // about to launch and a second (negative) set of space IDs that must not be 553 // present in the launched instance NICs. 554 func (env *maasEnviron) networkSpaceRequirements(ctx context.ProviderCallContext, endpointToProviderSpaceID map[string]corenetwork.Id, cons constraints.Value) (set.Strings, set.Strings, error) { 555 positiveSpaceIds := set.NewStrings() 556 negativeSpaceIds := set.NewStrings() 557 558 // Iterate the application bindings and add each bound space ID to the 559 // positive space set. 560 for _, providerSpaceID := range endpointToProviderSpaceID { 561 // The alpha space is not part of the MAAS space list. When the 562 // code that maps between space IDs and provider space IDs 563 // encounters a space that it cannot map, it passes the space 564 // name through. 565 if providerSpaceID == corenetwork.AlphaSpaceName { 566 continue 567 } 568 569 positiveSpaceIds.Add(string(providerSpaceID)) 570 } 571 572 // Convert space constraints into a list of space IDs to include and 573 // a list of space IDs to omit. 574 positiveSpaceNames, negativeSpaceNames := convertSpacesFromConstraints(cons.Spaces) 575 positiveSpaceInfo, negativeSpaceInfo, err := env.spaceNamesToSpaceInfo(ctx, positiveSpaceNames, negativeSpaceNames) 576 if err != nil { 577 // Spaces are not supported by this MAAS instance. 578 if errors.IsNotSupported(err) { 579 return nil, nil, nil 580 } 581 582 common.HandleCredentialError(IsAuthorisationFailure, err, ctx) 583 return nil, nil, errors.Trace(err) 584 } 585 586 // Append required space IDs from constraints. 587 for _, si := range positiveSpaceInfo { 588 if si.ProviderId == "" { 589 continue 590 } 591 positiveSpaceIds.Add(string(si.ProviderId)) 592 } 593 594 // Calculate negative space ID set and check for clashes with the positive set. 595 for _, si := range negativeSpaceInfo { 596 if si.ProviderId == "" { 597 continue 598 } 599 600 if positiveSpaceIds.Contains(string(si.ProviderId)) { 601 return nil, nil, errors.NewNotValid(nil, fmt.Sprintf("negative space %q from constraints clashes with required spaces for instance NICs", si.Name)) 602 } 603 604 negativeSpaceIds.Add(string(si.ProviderId)) 605 } 606 607 return positiveSpaceIds, negativeSpaceIds, nil 608 } 609 610 // acquireNode allocates a machine from MAAS. 611 func (env *maasEnviron) acquireNode( 612 ctx context.ProviderCallContext, 613 nodeName, zoneName, systemId string, 614 cons constraints.Value, 615 positiveSpaceIDs set.Strings, 616 negativeSpaceIDs set.Strings, 617 volumes []volumeInfo, 618 ) (*maasInstance, error) { 619 acquireParams := convertConstraints(cons) 620 addInterfaces(&acquireParams, positiveSpaceIDs, negativeSpaceIDs) 621 addStorage(&acquireParams, volumes) 622 acquireParams.AgentName = env.uuid 623 if zoneName != "" { 624 acquireParams.Zone = zoneName 625 } 626 if nodeName != "" { 627 acquireParams.Hostname = nodeName 628 } 629 if systemId != "" { 630 acquireParams.SystemId = systemId 631 } 632 machine, constraintMatches, err := env.maasController.AllocateMachine(acquireParams) 633 634 if err != nil { 635 common.HandleCredentialError(IsAuthorisationFailure, err, ctx) 636 return nil, errors.Trace(err) 637 } 638 return &maasInstance{ 639 machine: machine, 640 constraintMatches: constraintMatches, 641 environ: env, 642 }, nil 643 } 644 645 // DistributeInstances implements the state.InstanceDistributor policy. 646 func (env *maasEnviron) DistributeInstances( 647 ctx context.ProviderCallContext, candidates, distributionGroup []instance.Id, limitZones []string, 648 ) ([]instance.Id, error) { 649 return common.DistributeInstances(env, ctx, candidates, distributionGroup, limitZones) 650 } 651 652 // StartInstance is specified in the InstanceBroker interface. 653 func (env *maasEnviron) StartInstance( 654 ctx context.ProviderCallContext, 655 args environs.StartInstanceParams, 656 ) (_ *environs.StartInstanceResult, err error) { 657 658 availabilityZone := args.AvailabilityZone 659 var nodeName, systemId string 660 if args.Placement != "" { 661 placement, err := env.parsePlacement(ctx, args.Placement) 662 if err != nil { 663 return nil, environs.ZoneIndependentError(err) 664 } 665 // NOTE(axw) we wipe out args.AvailabilityZone if the 666 // user specified a specific node or system ID via 667 // placement, as placement must always take precedence. 668 switch { 669 case placement.systemId != "": 670 availabilityZone = "" 671 systemId = placement.systemId 672 case placement.nodeName != "": 673 availabilityZone = "" 674 nodeName = placement.nodeName 675 } 676 } 677 if availabilityZone != "" { 678 zones, err := env.AvailabilityZones(ctx) 679 if err != nil { 680 return nil, errors.Trace(err) 681 } 682 if err := errors.Trace(zones.Validate(availabilityZone)); err != nil { 683 return nil, errors.Trace(err) 684 } 685 logger.Debugf("attempting to acquire node in zone %q", availabilityZone) 686 } 687 688 // Storage. 689 volumes, err := buildMAASVolumeParameters(args.Volumes, args.Constraints) 690 if err != nil { 691 return nil, environs.ZoneIndependentError(errors.Annotate(err, "invalid volume parameters")) 692 } 693 694 // Calculate network space requirements. 695 positiveSpaceIDs, negativeSpaceIDs, err := env.networkSpaceRequirements(ctx, args.EndpointBindings, args.Constraints) 696 if err != nil { 697 return nil, errors.Trace(err) 698 } 699 700 inst, selectNodeErr := env.selectNode(ctx, 701 selectNodeArgs{ 702 Constraints: args.Constraints, 703 AvailabilityZone: availabilityZone, 704 NodeName: nodeName, 705 SystemId: systemId, 706 PositiveSpaceIDs: positiveSpaceIDs, 707 NegativeSpaceIDs: negativeSpaceIDs, 708 Volumes: volumes, 709 }) 710 if selectNodeErr != nil { 711 err := errors.Annotate(selectNodeErr, "failed to acquire node") 712 if selectNodeErr.noMatch && availabilityZone != "" { 713 // The error was due to MAAS not being able to 714 // find provide a machine matching the specified 715 // constraints in the zone; try again in another. 716 return nil, errors.Trace(err) 717 } 718 return nil, environs.ZoneIndependentError(err) 719 } 720 721 defer func() { 722 if err != nil { 723 if err := env.StopInstances(ctx, inst.Id()); err != nil { 724 logger.Errorf("error releasing failed instance: %v", err) 725 } 726 } 727 }() 728 729 hc, err := inst.hardwareCharacteristics() 730 if err != nil { 731 return nil, environs.ZoneIndependentError(err) 732 } 733 734 selectedTools, err := args.Tools.Match(tools.Filter{ 735 Arch: *hc.Arch, 736 }) 737 if err != nil { 738 return nil, environs.ZoneIndependentError(err) 739 } 740 if err := args.InstanceConfig.SetTools(selectedTools); err != nil { 741 return nil, environs.ZoneIndependentError(err) 742 } 743 744 hostname, err := inst.hostname() 745 if err != nil { 746 return nil, environs.ZoneIndependentError(err) 747 } 748 749 if err := instancecfg.FinishInstanceConfig(args.InstanceConfig, env.Config()); err != nil { 750 return nil, environs.ZoneIndependentError(err) 751 } 752 753 subnetsMap, err := env.subnetToSpaceIds(ctx) 754 if err != nil { 755 return nil, environs.ZoneIndependentError(err) 756 } 757 758 cloudcfg, err := env.newCloudinitConfig(hostname, args.InstanceConfig.Base.OS) 759 if err != nil { 760 return nil, environs.ZoneIndependentError(err) 761 } 762 763 userdata, err := providerinit.ComposeUserData(args.InstanceConfig, cloudcfg, MAASRenderer{}) 764 if err != nil { 765 return nil, environs.ZoneIndependentError(errors.Annotate( 766 err, "could not compose userdata for bootstrap node", 767 )) 768 } 769 logger.Debugf("maas user data; %d bytes", len(userdata)) 770 771 distroSeries, err := env.distroSeries(args) 772 if err != nil { 773 return nil, environs.ZoneIndependentError(err) 774 } 775 err = inst.machine.Start(gomaasapi.StartArgs{ 776 DistroSeries: distroSeries, 777 UserData: string(userdata), 778 }) 779 if err != nil { 780 return nil, environs.ZoneIndependentError(err) 781 } 782 783 domains, err := env.Domains(ctx) 784 if err != nil { 785 return nil, errors.Trace(err) 786 } 787 interfaces, err := maasNetworkInterfaces(ctx, inst, subnetsMap, domains...) 788 if err != nil { 789 return nil, environs.ZoneIndependentError(err) 790 } 791 env.tagInstance(inst, args.InstanceConfig) 792 793 displayName, err := inst.displayName() 794 if err != nil { 795 return nil, environs.ZoneIndependentError(err) 796 } 797 logger.Debugf("started instance %q", inst.Id()) 798 799 requestedVolumes := make([]names.VolumeTag, len(args.Volumes)) 800 for i, v := range args.Volumes { 801 requestedVolumes[i] = v.Tag 802 } 803 resultVolumes, resultAttachments, err := inst.volumes( 804 names.NewMachineTag(args.InstanceConfig.MachineId), 805 requestedVolumes, 806 ) 807 if err != nil { 808 return nil, environs.ZoneIndependentError(err) 809 } 810 if len(resultVolumes) != len(requestedVolumes) { 811 return nil, environs.ZoneIndependentError(errors.Errorf( 812 "requested %v storage volumes. %v returned", 813 len(requestedVolumes), len(resultVolumes), 814 )) 815 } 816 817 return &environs.StartInstanceResult{ 818 DisplayName: displayName, 819 Instance: inst, 820 Hardware: hc, 821 NetworkInfo: interfaces, 822 Volumes: resultVolumes, 823 VolumeAttachments: resultAttachments, 824 }, nil 825 } 826 827 func (env *maasEnviron) tagInstance(inst *maasInstance, instanceConfig *instancecfg.InstanceConfig) { 828 err := inst.machine.SetOwnerData(instanceConfig.Tags) 829 if err != nil { 830 logger.Errorf("could not set owner data for instance: %v", err) 831 } 832 } 833 834 func (env *maasEnviron) distroSeries(args environs.StartInstanceParams) (string, error) { 835 if args.Constraints.HasImageID() { 836 return *args.Constraints.ImageID, nil 837 } 838 return corebase.GetSeriesFromBase(args.InstanceConfig.Base) 839 } 840 841 func (env *maasEnviron) waitForNodeDeployment(ctx context.ProviderCallContext, id instance.Id, timeout time.Duration) error { 842 retryStrategy := env.longRetryStrategy 843 retryStrategy.MaxDuration = timeout 844 retryStrategy.IsFatalError = func(err error) bool { 845 if errors.IsNotProvisioned(err) { 846 return true 847 } 848 if denied := common.MaybeHandleCredentialError(IsAuthorisationFailure, err, ctx); denied { 849 return true 850 } 851 return false 852 } 853 retryStrategy.NotifyFunc = func(lastErr error, attempts int) { 854 if errors.IsNotFound(lastErr) { 855 logger.Warningf("failed to get instance from provider attempt %d", attempts) 856 } 857 } 858 retryStrategy.Func = func() error { 859 machine, err := env.getInstance(ctx, id) 860 if err != nil { 861 return err 862 } 863 stat := machine.Status(ctx) 864 if stat.Status == status.Running { 865 return nil 866 } 867 if stat.Status == status.ProvisioningError { 868 return errors.NewNotProvisioned(nil, fmt.Sprintf("instance %q failed to deploy", id)) 869 } 870 return errors.NewNotYetAvailable(nil, "Not yet provisioned") 871 } 872 err := retry.Call(retryStrategy) 873 if retry.IsAttemptsExceeded(err) || retry.IsDurationExceeded(err) { 874 return errors.Errorf("instance %q is started but not deployed", id) 875 } 876 return errors.Trace(err) 877 } 878 879 func deploymentStatusCall(nodes gomaasapi.MAASObject, ids ...instance.Id) (gomaasapi.JSONObject, error) { 880 filter := getSystemIdValues("nodes", ids) 881 return nodes.CallGet("deployment_status", filter) 882 } 883 884 type selectNodeArgs struct { 885 AvailabilityZone string 886 NodeName string 887 SystemId string 888 Constraints constraints.Value 889 PositiveSpaceIDs set.Strings 890 NegativeSpaceIDs set.Strings 891 Volumes []volumeInfo 892 } 893 894 type selectNodeError struct { 895 error 896 noMatch bool 897 } 898 899 func (env *maasEnviron) selectNode(ctx context.ProviderCallContext, args selectNodeArgs) (*maasInstance, *selectNodeError) { 900 inst, err := env.acquireNode( 901 ctx, 902 args.NodeName, 903 args.AvailabilityZone, 904 args.SystemId, 905 args.Constraints, 906 args.PositiveSpaceIDs, 907 args.NegativeSpaceIDs, 908 args.Volumes, 909 ) 910 if err != nil { 911 return nil, &selectNodeError{ 912 error: errors.Trace(err), 913 noMatch: gomaasapi.IsNoMatchError(err), 914 } 915 } 916 return inst, nil 917 } 918 919 // newCloudinitConfig creates a cloudinit.Config structure suitable as a base 920 // for initialising a MAAS node. 921 func (env *maasEnviron) newCloudinitConfig(hostname, osname string) (cloudinit.CloudConfig, error) { 922 cloudcfg, err := cloudinit.New(osname) 923 if err != nil { 924 return nil, err 925 } 926 927 info := machineInfo{hostname} 928 runCmd, err := info.cloudinitRunCmd(cloudcfg) 929 if err != nil { 930 return nil, errors.Trace(err) 931 } 932 933 operatingSystem := ostype.OSTypeForName(osname) 934 switch operatingSystem { 935 case ostype.Ubuntu: 936 cloudcfg.SetSystemUpdate(true) 937 cloudcfg.AddScripts("set -xe", runCmd) 938 // DisableNetworkManagement can still disable the bridge(s) creation. 939 if on, set := env.Config().DisableNetworkManagement(); on && set { 940 logger.Infof( 941 "network management disabled - not using %q bridge for containers", 942 instancecfg.DefaultBridgeName, 943 ) 944 break 945 } 946 cloudcfg.AddPackage("bridge-utils") 947 } 948 return cloudcfg, nil 949 } 950 951 func (env *maasEnviron) releaseNodes(ctx context.ProviderCallContext, ids []instance.Id, recurse bool) error { 952 args := gomaasapi.ReleaseMachinesArgs{ 953 SystemIDs: instanceIdsToSystemIDs(ids), 954 Comment: "Released by Juju MAAS provider", 955 } 956 err := env.maasController.ReleaseMachines(args) 957 958 denied := common.MaybeHandleCredentialError(IsAuthorisationFailure, err, ctx) 959 switch { 960 case err == nil: 961 return nil 962 case gomaasapi.IsCannotCompleteError(err): 963 // CannotCompleteError means a node couldn't be released due to 964 // a state conflict. Likely it's already released or disk 965 // erasing. We're assuming this error *only* means it's 966 // safe to assume the instance is already released. 967 // MaaS also releases (or attempts) all nodes, and raises 968 // a single error on failure. So even with an error 409, all 969 // nodes have been released. 970 logger.Infof("ignoring error while releasing nodes (%v); all nodes released OK", err) 971 return nil 972 case gomaasapi.IsBadRequestError(err), denied: 973 // a status code of 400 or 403 means one of the nodes 974 // couldn't be found and none have been released. We have to 975 // release all the ones we can individually. 976 if !recurse { 977 // this node has already been released and we're golden 978 return nil 979 } 980 return env.releaseNodesIndividually(ctx, ids) 981 982 default: 983 return errors.Annotatef(err, "cannot release nodes") 984 } 985 } 986 987 func (env *maasEnviron) releaseNodesIndividually(ctx context.ProviderCallContext, ids []instance.Id) error { 988 var lastErr error 989 for _, id := range ids { 990 err := env.releaseNodes(ctx, []instance.Id{id}, false) 991 if err != nil { 992 lastErr = err 993 logger.Errorf("error while releasing node %v (%v)", id, err) 994 if denied := common.MaybeHandleCredentialError(IsAuthorisationFailure, err, ctx); denied { 995 break 996 } 997 } 998 } 999 return errors.Trace(lastErr) 1000 } 1001 1002 func instanceIdsToSystemIDs(ids []instance.Id) []string { 1003 systemIDs := make([]string, len(ids)) 1004 for index, id := range ids { 1005 systemIDs[index] = string(id) 1006 } 1007 return systemIDs 1008 } 1009 1010 // StopInstances is specified in the InstanceBroker interface. 1011 func (env *maasEnviron) StopInstances(ctx context.ProviderCallContext, ids ...instance.Id) error { 1012 // Shortcut to exit quickly if 'instances' is an empty slice or nil. 1013 if len(ids) == 0 { 1014 return nil 1015 } 1016 1017 err := env.releaseNodes(ctx, ids, true) 1018 if err != nil { 1019 return errors.Trace(err) 1020 } 1021 return common.RemoveStateInstances(env.Storage(), ids...) 1022 1023 } 1024 1025 // Instances returns the instances.Instance objects corresponding to the given 1026 // slice of instance.Id. The error is ErrNoInstances if no instances 1027 // were found. 1028 func (env *maasEnviron) Instances(ctx context.ProviderCallContext, ids []instance.Id) ([]instances.Instance, error) { 1029 if len(ids) == 0 { 1030 // This would be treated as "return all instances" below, so 1031 // treat it as a special case. 1032 // The interface requires us to return this particular error 1033 // if no instances were found. 1034 return nil, environs.ErrNoInstances 1035 } 1036 acquired, err := env.acquiredInstances(ctx, ids) 1037 if err != nil { 1038 return nil, errors.Trace(err) 1039 } 1040 if len(acquired) == 0 { 1041 return nil, environs.ErrNoInstances 1042 } 1043 1044 idMap := make(map[instance.Id]instances.Instance) 1045 for _, inst := range acquired { 1046 idMap[inst.Id()] = inst 1047 } 1048 1049 missing := false 1050 result := make([]instances.Instance, len(ids)) 1051 for index, id := range ids { 1052 val, ok := idMap[id] 1053 if !ok { 1054 missing = true 1055 continue 1056 } 1057 result[index] = val 1058 } 1059 1060 if missing { 1061 return result, environs.ErrPartialInstances 1062 } 1063 return result, nil 1064 } 1065 1066 // acquireInstances calls the MAAS API to list acquired nodes. 1067 // 1068 // The "ids" slice is a filter for specific instance IDs. 1069 // Due to how this works in the HTTP API, an empty "ids" 1070 // matches all instances (not none as you might expect). 1071 func (env *maasEnviron) acquiredInstances(ctx context.ProviderCallContext, ids []instance.Id) ([]instances.Instance, error) { 1072 args := gomaasapi.MachinesArgs{ 1073 AgentName: env.uuid, 1074 SystemIDs: instanceIdsToSystemIDs(ids), 1075 } 1076 1077 maasInstances, err := env.instances(ctx, args) 1078 if err != nil { 1079 return nil, errors.Trace(err) 1080 } 1081 1082 inst := make([]instances.Instance, len(maasInstances)) 1083 for i, mi := range maasInstances { 1084 inst[i] = mi 1085 } 1086 return inst, nil 1087 } 1088 1089 func (env *maasEnviron) instances(ctx context.ProviderCallContext, args gomaasapi.MachinesArgs) ([]*maasInstance, error) { 1090 machines, err := env.maasController.Machines(args) 1091 if err != nil { 1092 common.HandleCredentialError(IsAuthorisationFailure, err, ctx) 1093 return nil, errors.Trace(err) 1094 } 1095 1096 inst := make([]*maasInstance, len(machines)) 1097 for index, machine := range machines { 1098 inst[index] = &maasInstance{machine: machine, environ: env} 1099 } 1100 return inst, nil 1101 } 1102 1103 func (env *maasEnviron) getInstance(ctx context.ProviderCallContext, instId instance.Id) (instances.Instance, error) { 1104 instances, err := env.acquiredInstances(ctx, []instance.Id{instId}) 1105 if err != nil { 1106 // This path can never trigger on MAAS 2, but MAAS 2 doesn't 1107 // return an error for a machine not found, it just returns 1108 // empty results. The clause below catches that. 1109 if maasErr, ok := errors.Cause(err).(gomaasapi.ServerError); ok && maasErr.StatusCode == http.StatusNotFound { 1110 return nil, errors.NotFoundf("instance %q", instId) 1111 } 1112 return nil, errors.Annotatef(err, "getting instance %q", instId) 1113 } 1114 if len(instances) == 0 { 1115 return nil, errors.NotFoundf("instance %q", instId) 1116 } 1117 inst := instances[0] 1118 return inst, nil 1119 } 1120 1121 // subnetToSpaceIds fetches the spaces from MAAS and builds a map of subnets to 1122 // space ids. 1123 func (env *maasEnviron) subnetToSpaceIds(ctx context.ProviderCallContext) (map[string]corenetwork.Id, error) { 1124 subnetsMap := make(map[string]corenetwork.Id) 1125 spaces, err := env.Spaces(ctx) 1126 if err != nil { 1127 return subnetsMap, errors.Trace(err) 1128 } 1129 for _, space := range spaces { 1130 for _, subnet := range space.Subnets { 1131 subnetsMap[subnet.CIDR] = space.ProviderId 1132 } 1133 } 1134 return subnetsMap, nil 1135 } 1136 1137 // Spaces returns all the spaces, that have subnets, known to the provider. 1138 // Space name is not filled in as the provider doesn't know the juju name for 1139 // the space. 1140 func (env *maasEnviron) Spaces(ctx context.ProviderCallContext) (corenetwork.SpaceInfos, error) { 1141 spaces, err := env.maasController.Spaces() 1142 if err != nil { 1143 common.HandleCredentialError(IsAuthorisationFailure, err, ctx) 1144 return nil, errors.Trace(err) 1145 } 1146 var result []corenetwork.SpaceInfo 1147 for _, space := range spaces { 1148 if len(space.Subnets()) == 0 { 1149 continue 1150 } 1151 outSpace := corenetwork.SpaceInfo{ 1152 Name: corenetwork.SpaceName(space.Name()), 1153 ProviderId: corenetwork.Id(strconv.Itoa(space.ID())), 1154 Subnets: make([]corenetwork.SubnetInfo, len(space.Subnets())), 1155 } 1156 for i, subnet := range space.Subnets() { 1157 subnetInfo := corenetwork.SubnetInfo{ 1158 ProviderId: corenetwork.Id(strconv.Itoa(subnet.ID())), 1159 VLANTag: subnet.VLAN().VID(), 1160 CIDR: subnet.CIDR(), 1161 ProviderSpaceId: corenetwork.Id(strconv.Itoa(space.ID())), 1162 } 1163 outSpace.Subnets[i] = subnetInfo 1164 } 1165 result = append(result, outSpace) 1166 } 1167 return result, nil 1168 } 1169 1170 // Subnets returns basic information about the specified subnets known 1171 // by the provider for the specified instance. subnetIds must not be 1172 // empty. Implements NetworkingEnviron.Subnets. 1173 func (env *maasEnviron) Subnets( 1174 ctx context.ProviderCallContext, instId instance.Id, subnetIds []corenetwork.Id, 1175 ) ([]corenetwork.SubnetInfo, error) { 1176 var subnets []corenetwork.SubnetInfo 1177 if instId == instance.UnknownId { 1178 spaces, err := env.Spaces(ctx) 1179 if err != nil { 1180 return nil, errors.Trace(err) 1181 } 1182 for _, space := range spaces { 1183 subnets = append(subnets, space.Subnets...) 1184 } 1185 } else { 1186 var err error 1187 subnets, err = env.filteredSubnets2(ctx, instId) 1188 if err != nil { 1189 return nil, errors.Trace(err) 1190 } 1191 } 1192 1193 if len(subnetIds) == 0 { 1194 return subnets, nil 1195 } 1196 var result []corenetwork.SubnetInfo 1197 subnetMap := make(map[string]bool) 1198 for _, subnetId := range subnetIds { 1199 subnetMap[string(subnetId)] = false 1200 } 1201 for _, subnet := range subnets { 1202 _, ok := subnetMap[string(subnet.ProviderId)] 1203 if !ok { 1204 // This id is not what we're looking for. 1205 continue 1206 } 1207 subnetMap[string(subnet.ProviderId)] = true 1208 result = append(result, subnet) 1209 } 1210 1211 return result, checkNotFound(subnetMap) 1212 } 1213 1214 func (env *maasEnviron) filteredSubnets2( 1215 ctx context.ProviderCallContext, instId instance.Id, 1216 ) ([]corenetwork.SubnetInfo, error) { 1217 args := gomaasapi.MachinesArgs{ 1218 AgentName: env.uuid, 1219 SystemIDs: []string{string(instId)}, 1220 } 1221 machines, err := env.maasController.Machines(args) 1222 if err != nil { 1223 common.HandleCredentialError(IsAuthorisationFailure, err, ctx) 1224 return nil, errors.Trace(err) 1225 } 1226 if len(machines) == 0 { 1227 return nil, errors.NotFoundf("machine %v", instId) 1228 } else if len(machines) > 1 { 1229 return nil, errors.Errorf("unexpected response getting machine details %v: %v", instId, machines) 1230 } 1231 1232 machine := machines[0] 1233 spaceMap, err := env.buildSpaceMap(ctx) 1234 if err != nil { 1235 return nil, errors.Trace(err) 1236 } 1237 var result []corenetwork.SubnetInfo 1238 for _, iface := range machine.InterfaceSet() { 1239 for _, link := range iface.Links() { 1240 subnet := link.Subnet() 1241 space, ok := spaceMap[subnet.Space()] 1242 if !ok { 1243 return nil, errors.Errorf("missing space %v on subnet %v", subnet.Space(), subnet.CIDR()) 1244 } 1245 subnetInfo := corenetwork.SubnetInfo{ 1246 ProviderId: corenetwork.Id(strconv.Itoa(subnet.ID())), 1247 VLANTag: subnet.VLAN().VID(), 1248 CIDR: subnet.CIDR(), 1249 ProviderSpaceId: space.ProviderId, 1250 } 1251 result = append(result, subnetInfo) 1252 } 1253 } 1254 return result, nil 1255 } 1256 1257 func checkNotFound(subnetIdSet map[string]bool) error { 1258 var notFound []string 1259 for subnetId, found := range subnetIdSet { 1260 if !found { 1261 notFound = append(notFound, subnetId) 1262 } 1263 } 1264 if len(notFound) != 0 { 1265 return errors.Errorf("failed to find the following subnets: %v", strings.Join(notFound, ", ")) 1266 } 1267 return nil 1268 } 1269 1270 // AllInstances implements environs.InstanceBroker. 1271 func (env *maasEnviron) AllInstances(ctx context.ProviderCallContext) ([]instances.Instance, error) { 1272 return env.acquiredInstances(ctx, nil) 1273 } 1274 1275 // AllRunningInstances implements environs.InstanceBroker. 1276 func (env *maasEnviron) AllRunningInstances(ctx context.ProviderCallContext) ([]instances.Instance, error) { 1277 // We always get all instances here, so "all" is the same as "running". 1278 return env.AllInstances(ctx) 1279 } 1280 1281 // Storage is defined by the Environ interface. 1282 func (env *maasEnviron) Storage() storage.Storage { 1283 env.ecfgMutex.Lock() 1284 defer env.ecfgMutex.Unlock() 1285 return env.storageUnlocked 1286 } 1287 1288 func (env *maasEnviron) Destroy(ctx context.ProviderCallContext) error { 1289 if err := common.Destroy(env, ctx); err != nil { 1290 return errors.Trace(err) 1291 } 1292 return env.Storage().RemoveAll() 1293 } 1294 1295 // DestroyController implements the Environ interface. 1296 func (env *maasEnviron) DestroyController(ctx context.ProviderCallContext, controllerUUID string) error { 1297 // TODO(wallyworld): destroy hosted model resources 1298 return env.Destroy(ctx) 1299 } 1300 1301 func (*maasEnviron) Provider() environs.EnvironProvider { 1302 return &providerInstance 1303 } 1304 1305 func (env *maasEnviron) AllocateContainerAddresses(ctx context.ProviderCallContext, hostInstanceID instance.Id, containerTag names.MachineTag, preparedInfo corenetwork.InterfaceInfos) (corenetwork.InterfaceInfos, error) { 1306 if len(preparedInfo) == 0 { 1307 return nil, errors.Errorf("no prepared info to allocate") 1308 } 1309 1310 logger.Debugf("using prepared container info: %+v", preparedInfo) 1311 args := gomaasapi.MachinesArgs{ 1312 AgentName: env.uuid, 1313 SystemIDs: []string{string(hostInstanceID)}, 1314 } 1315 machines, err := env.maasController.Machines(args) 1316 if err != nil { 1317 return nil, errors.Trace(err) 1318 } 1319 if len(machines) != 1 { 1320 return nil, errors.Errorf("failed to identify unique machine with ID %q; got %v", hostInstanceID, machines) 1321 } 1322 machine := machines[0] 1323 deviceName, err := env.namespace.Hostname(containerTag.Id()) 1324 if err != nil { 1325 return nil, errors.Trace(err) 1326 } 1327 params, err := env.prepareDeviceDetails(deviceName, machine, preparedInfo) 1328 if err != nil { 1329 return nil, errors.Trace(err) 1330 } 1331 1332 // Check to see if we've already tried to allocate information for this device: 1333 device, err := env.checkForExistingDevice(params) 1334 if err != nil { 1335 return nil, errors.Trace(err) 1336 } 1337 if device == nil { 1338 device, err = env.createAndPopulateDevice(params) 1339 if err != nil { 1340 return nil, errors.Annotatef(err, 1341 "failed to create MAAS device for %q", 1342 params.Name) 1343 } 1344 } 1345 1346 // TODO(jam): the old code used to reload the device from its SystemID() 1347 nameToParentName := make(map[string]string) 1348 for _, nic := range preparedInfo { 1349 nameToParentName[nic.InterfaceName] = nic.ParentInterfaceName 1350 } 1351 interfaces, err := env.deviceInterfaceInfo(device, nameToParentName, params.CIDRToStaticRoutes) 1352 if err != nil { 1353 return nil, errors.Annotate(err, "cannot get device interfaces") 1354 } 1355 return interfaces, nil 1356 } 1357 1358 func (env *maasEnviron) ReleaseContainerAddresses(ctx context.ProviderCallContext, interfaces []corenetwork.ProviderInterfaceInfo) error { 1359 hwAddresses := make([]string, len(interfaces)) 1360 for i, info := range interfaces { 1361 hwAddresses[i] = info.HardwareAddress 1362 } 1363 1364 devices, err := env.maasController.Devices(gomaasapi.DevicesArgs{MACAddresses: hwAddresses}) 1365 if err != nil { 1366 common.HandleCredentialError(IsAuthorisationFailure, err, ctx) 1367 return errors.Trace(err) 1368 } 1369 // If one device matched on multiple MAC addresses (like for 1370 // multi-nic containers) it will be in the slice multiple 1371 // times. Skip devices we've seen already. 1372 seen := set.NewStrings() 1373 for _, device := range devices { 1374 if seen.Contains(device.SystemID()) { 1375 continue 1376 } 1377 seen.Add(device.SystemID()) 1378 1379 err = device.Delete() 1380 if err != nil { 1381 return errors.Annotatef(err, "deleting device %s", device.SystemID()) 1382 } 1383 } 1384 return nil 1385 } 1386 1387 // AdoptResources updates all the instances to indicate they 1388 // are now associated with the specified controller. 1389 func (env *maasEnviron) AdoptResources(ctx context.ProviderCallContext, controllerUUID string, _ version.Number) error { 1390 allInstances, err := env.AllInstances(ctx) 1391 if err != nil { 1392 return errors.Trace(err) 1393 } 1394 var failed []instance.Id 1395 for _, inst := range allInstances { 1396 maasInst, ok := inst.(*maasInstance) 1397 if !ok { 1398 // This should never happen. 1399 return errors.Errorf("instance %q wasn't a maasInstance", inst.Id()) 1400 } 1401 // From the MAAS docs: "[SetOwnerData] will not remove any 1402 // previous keys unless explicitly passed with an empty 1403 // string." So not passing all of the keys here is fine. 1404 // https://maas.ubuntu.com/docs2.0/api.html#machine 1405 err := maasInst.machine.SetOwnerData(map[string]string{tags.JujuController: controllerUUID}) 1406 if err != nil { 1407 logger.Errorf("error setting controller uuid tag for %q: %v", inst.Id(), err) 1408 failed = append(failed, inst.Id()) 1409 } 1410 } 1411 1412 if failed != nil { 1413 return errors.Errorf("failed to update controller for some instances: %v", failed) 1414 } 1415 return nil 1416 } 1417 1418 // ProviderSpaceInfo implements environs.NetworkingEnviron. 1419 func (*maasEnviron) ProviderSpaceInfo( 1420 ctx context.ProviderCallContext, space *corenetwork.SpaceInfo, 1421 ) (*environs.ProviderSpaceInfo, error) { 1422 return nil, errors.NotSupportedf("provider space info") 1423 } 1424 1425 // AreSpacesRoutable implements environs.NetworkingEnviron. 1426 func (*maasEnviron) AreSpacesRoutable(ctx context.ProviderCallContext, space1, space2 *environs.ProviderSpaceInfo) (bool, error) { 1427 return false, nil 1428 } 1429 1430 // SuperSubnets implements environs.SuperSubnets 1431 func (*maasEnviron) SuperSubnets(ctx context.ProviderCallContext) ([]string, error) { 1432 return nil, errors.NotSupportedf("super subnets") 1433 } 1434 1435 // Domains gets the domains managed by MAAS. We only need the name of the 1436 // domain at present. If more information is needed this function can be 1437 // updated to parse and return a structure. Client code would need to be 1438 // updated. 1439 func (env *maasEnviron) Domains(ctx context.ProviderCallContext) ([]string, error) { 1440 maasDomains, err := env.maasController.Domains() 1441 if err != nil { 1442 common.HandleCredentialError(IsAuthorisationFailure, err, ctx) 1443 return nil, errors.Trace(err) 1444 } 1445 var result []string 1446 for _, domain := range maasDomains { 1447 result = append(result, domain.Name()) 1448 } 1449 return result, nil 1450 }