github.com/wallyworld/juju@v0.0.0-20161013125918-6cf1bc9d917a/provider/azure/environ.go (about) 1 // Copyright 2015 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 4 package azure 5 6 import ( 7 "fmt" 8 "net/http" 9 "net/url" 10 "sort" 11 "strings" 12 "sync" 13 14 "github.com/Azure/azure-sdk-for-go/arm/compute" 15 "github.com/Azure/azure-sdk-for-go/arm/network" 16 "github.com/Azure/azure-sdk-for-go/arm/resources/resources" 17 "github.com/Azure/azure-sdk-for-go/arm/storage" 18 azurestorage "github.com/Azure/azure-sdk-for-go/storage" 19 "github.com/Azure/go-autorest/autorest" 20 "github.com/Azure/go-autorest/autorest/to" 21 "github.com/juju/errors" 22 "github.com/juju/loggo" 23 "github.com/juju/utils/arch" 24 "github.com/juju/utils/os" 25 jujuseries "github.com/juju/utils/series" 26 "gopkg.in/juju/names.v2" 27 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/instances" 34 "github.com/juju/juju/environs/simplestreams" 35 "github.com/juju/juju/environs/tags" 36 "github.com/juju/juju/instance" 37 jujunetwork "github.com/juju/juju/network" 38 "github.com/juju/juju/provider/azure/internal/armtemplates" 39 internalazurestorage "github.com/juju/juju/provider/azure/internal/azurestorage" 40 "github.com/juju/juju/provider/azure/internal/errorutils" 41 "github.com/juju/juju/provider/azure/internal/tracing" 42 "github.com/juju/juju/provider/common" 43 "github.com/juju/juju/state" 44 "github.com/juju/juju/tools" 45 ) 46 47 const ( 48 jujuMachineNameTag = tags.JujuTagPrefix + "machine-name" 49 50 // defaultRootDiskSize is the default root disk size to give 51 // to a VM, if none is specified. 52 defaultRootDiskSize = 30 * 1024 // 30 GiB 53 54 // serviceErrorCodeDeploymentCannotBeCancelled is the error code for 55 // service errors in response to an attempt to cancel a deployment 56 // that cannot be cancelled. 57 serviceErrorCodeDeploymentCannotBeCancelled = "DeploymentCannotBeCancelled" 58 59 // controllerAvailabilitySet is the name of the availability set 60 // used for controller machines. 61 controllerAvailabilitySet = "juju-controller" 62 ) 63 64 type azureEnviron struct { 65 // provider is the azureEnvironProvider used to open this environment. 66 provider *azureEnvironProvider 67 68 // cloud defines the cloud configuration for this environment. 69 cloud environs.CloudSpec 70 71 // location is the canonicalized location name. Use this instead 72 // of cloud.Region in API calls. 73 location string 74 75 // subscriptionId is the Azure account subscription ID. 76 subscriptionId string 77 78 // storageEndpoint is the Azure storage endpoint. This is the host 79 // portion of the storage endpoint URL only; use this instead of 80 // cloud.StorageEndpoint in API calls. 81 storageEndpoint string 82 83 // resourceGroup is the name of the Resource Group in the Azure 84 // subscription that corresponds to the environment. 85 resourceGroup string 86 87 // envName is the name of the environment. 88 envName string 89 90 // authorizer is the authorizer we use for Azure. 91 authorizer *cloudSpecAuth 92 93 compute compute.ManagementClient 94 resources resources.ManagementClient 95 storage storage.ManagementClient 96 network network.ManagementClient 97 storageClient azurestorage.Client 98 storageAccountName string 99 100 mu sync.Mutex 101 config *azureModelConfig 102 instanceTypes map[string]instances.InstanceType 103 storageAccount *storage.Account 104 storageAccountKey *storage.AccountKey 105 } 106 107 var _ environs.Environ = (*azureEnviron)(nil) 108 var _ state.Prechecker = (*azureEnviron)(nil) 109 110 // newEnviron creates a new azureEnviron. 111 func newEnviron( 112 provider *azureEnvironProvider, 113 cloud environs.CloudSpec, 114 cfg *config.Config, 115 ) (*azureEnviron, error) { 116 117 // The Azure storage code wants the endpoint host only, not the URL. 118 storageEndpointURL, err := url.Parse(cloud.StorageEndpoint) 119 if err != nil { 120 return nil, errors.Annotate(err, "parsing storage endpoint URL") 121 } 122 123 env := azureEnviron{ 124 provider: provider, 125 cloud: cloud, 126 location: canonicalLocation(cloud.Region), 127 storageEndpoint: storageEndpointURL.Host, 128 } 129 if err := env.initEnviron(); err != nil { 130 return nil, errors.Trace(err) 131 } 132 133 if err := env.SetConfig(cfg); err != nil { 134 return nil, errors.Trace(err) 135 } 136 137 modelTag := names.NewModelTag(cfg.UUID()) 138 env.resourceGroup = resourceGroupName(modelTag, cfg.Name()) 139 env.envName = cfg.Name() 140 141 // We need a deterministic storage account name, so that we can 142 // defer creation of the storage account to the VM deployment, 143 // and retain the ability to create multiple deployments in 144 // parallel. 145 // 146 // We use the last 20 non-hyphen hex characters of the model's 147 // UUID as the storage account name, prefixed with "juju". The 148 // probability of clashing with another storage account should 149 // be negligible. 150 uuidAlphaNumeric := strings.Replace(env.config.Config.UUID(), "-", "", -1) 151 env.storageAccountName = "juju" + uuidAlphaNumeric[len(uuidAlphaNumeric)-20:] 152 153 return &env, nil 154 } 155 156 func (env *azureEnviron) initEnviron() error { 157 credAttrs := env.cloud.Credential.Attributes() 158 env.subscriptionId = credAttrs[credAttrSubscriptionId] 159 env.authorizer = &cloudSpecAuth{ 160 cloud: env.cloud, 161 sender: env.provider.config.Sender, 162 } 163 164 env.compute = compute.NewWithBaseURI(env.cloud.Endpoint, env.subscriptionId) 165 env.resources = resources.NewWithBaseURI(env.cloud.Endpoint, env.subscriptionId) 166 env.storage = storage.NewWithBaseURI(env.cloud.Endpoint, env.subscriptionId) 167 env.network = network.NewWithBaseURI(env.cloud.Endpoint, env.subscriptionId) 168 clients := map[string]*autorest.Client{ 169 "azure.compute": &env.compute.Client, 170 "azure.resources": &env.resources.Client, 171 "azure.storage": &env.storage.Client, 172 "azure.network": &env.network.Client, 173 } 174 for id, client := range clients { 175 client.Authorizer = env.authorizer 176 logger := loggo.GetLogger(id) 177 if env.provider.config.Sender != nil { 178 client.Sender = env.provider.config.Sender 179 } 180 client.ResponseInspector = tracing.RespondDecorator(logger) 181 client.RequestInspector = tracing.PrepareDecorator(logger) 182 if env.provider.config.RequestInspector != nil { 183 tracer := client.RequestInspector 184 inspector := env.provider.config.RequestInspector 185 client.RequestInspector = func(p autorest.Preparer) autorest.Preparer { 186 p = tracer(p) 187 p = inspector(p) 188 return p 189 } 190 } 191 } 192 return nil 193 } 194 195 // PrepareForBootstrap is part of the Environ interface. 196 func (env *azureEnviron) PrepareForBootstrap(ctx environs.BootstrapContext) error { 197 if ctx.ShouldVerifyCredentials() { 198 if err := verifyCredentials(env); err != nil { 199 return errors.Trace(err) 200 } 201 } 202 return nil 203 } 204 205 // Create is part of the Environ interface. 206 func (env *azureEnviron) Create(args environs.CreateParams) error { 207 if err := verifyCredentials(env); err != nil { 208 return errors.Trace(err) 209 } 210 return errors.Trace(env.initResourceGroup(args.ControllerUUID)) 211 } 212 213 // Bootstrap is part of the Environ interface. 214 func (env *azureEnviron) Bootstrap( 215 ctx environs.BootstrapContext, 216 args environs.BootstrapParams, 217 ) (*environs.BootstrapResult, error) { 218 if err := env.initResourceGroup(args.ControllerConfig.ControllerUUID()); err != nil { 219 return nil, errors.Annotate(err, "creating controller resource group") 220 } 221 result, err := common.Bootstrap(ctx, env, args) 222 if err != nil { 223 logger.Errorf("bootstrap failed, destroying model: %v", err) 224 if err := env.Destroy(); err != nil { 225 logger.Errorf("failed to destroy model: %v", err) 226 } 227 return nil, errors.Trace(err) 228 } 229 return result, nil 230 } 231 232 // BootstrapMessage is part of the Environ interface. 233 func (env *azureEnviron) BootstrapMessage() string { 234 return "" 235 } 236 237 // initResourceGroup creates a resource group for this environment. 238 func (env *azureEnviron) initResourceGroup(controllerUUID string) error { 239 location := env.location 240 resourceGroupsClient := resources.GroupsClient{env.resources} 241 242 env.mu.Lock() 243 tags := tags.ResourceTags( 244 names.NewModelTag(env.config.Config.UUID()), 245 names.NewControllerTag(controllerUUID), 246 env.config, 247 ) 248 env.mu.Unlock() 249 250 logger.Debugf("creating resource group %q", env.resourceGroup) 251 err := env.callAPI(func() (autorest.Response, error) { 252 group, err := resourceGroupsClient.CreateOrUpdate(env.resourceGroup, resources.ResourceGroup{ 253 Location: to.StringPtr(location), 254 Tags: to.StringMapPtr(tags), 255 }) 256 return group.Response, err 257 }) 258 return errors.Annotate(err, "creating resource group") 259 } 260 261 // ControllerInstances is specified in the Environ interface. 262 func (env *azureEnviron) ControllerInstances(controllerUUID string) ([]instance.Id, error) { 263 instances, err := env.allInstances(env.resourceGroup, false, true) 264 if err != nil { 265 return nil, err 266 } 267 if len(instances) == 0 { 268 return nil, environs.ErrNoInstances 269 } 270 ids := make([]instance.Id, len(instances)) 271 for i, inst := range instances { 272 ids[i] = inst.Id() 273 } 274 return ids, nil 275 } 276 277 // Config is specified in the Environ interface. 278 func (env *azureEnviron) Config() *config.Config { 279 env.mu.Lock() 280 defer env.mu.Unlock() 281 return env.config.Config 282 } 283 284 // SetConfig is specified in the Environ interface. 285 func (env *azureEnviron) SetConfig(cfg *config.Config) error { 286 env.mu.Lock() 287 defer env.mu.Unlock() 288 289 var old *config.Config 290 if env.config != nil { 291 old = env.config.Config 292 } 293 ecfg, err := validateConfig(cfg, old) 294 if err != nil { 295 return err 296 } 297 env.config = ecfg 298 299 return nil 300 } 301 302 // ConstraintsValidator is defined on the Environs interface. 303 func (env *azureEnviron) ConstraintsValidator() (constraints.Validator, error) { 304 instanceTypes, err := env.getInstanceTypes() 305 if err != nil { 306 return nil, err 307 } 308 instTypeNames := make([]string, 0, len(instanceTypes)) 309 for instTypeName := range instanceTypes { 310 instTypeNames = append(instTypeNames, instTypeName) 311 } 312 sort.Strings(instTypeNames) 313 314 validator := constraints.NewValidator() 315 validator.RegisterUnsupported([]string{ 316 constraints.CpuPower, 317 constraints.Tags, 318 constraints.VirtType, 319 }) 320 validator.RegisterVocabulary( 321 constraints.Arch, 322 []string{arch.AMD64}, 323 ) 324 validator.RegisterVocabulary( 325 constraints.InstanceType, 326 instTypeNames, 327 ) 328 validator.RegisterConflicts( 329 []string{constraints.InstanceType}, 330 []string{ 331 constraints.Mem, 332 constraints.Cores, 333 constraints.Arch, 334 }, 335 ) 336 return validator, nil 337 } 338 339 // PrecheckInstance is defined on the state.Prechecker interface. 340 func (env *azureEnviron) PrecheckInstance(series string, cons constraints.Value, placement string) error { 341 if placement != "" { 342 return fmt.Errorf("unknown placement directive: %s", placement) 343 } 344 if !cons.HasInstanceType() { 345 return nil 346 } 347 // Constraint has an instance-type constraint so let's see if it is valid. 348 instanceTypes, err := env.getInstanceTypes() 349 if err != nil { 350 return err 351 } 352 for _, instanceType := range instanceTypes { 353 if instanceType.Name == *cons.InstanceType { 354 return nil 355 } 356 } 357 return fmt.Errorf("invalid instance type %q", *cons.InstanceType) 358 } 359 360 // MaintainInstance is specified in the InstanceBroker interface. 361 func (*azureEnviron) MaintainInstance(args environs.StartInstanceParams) error { 362 return nil 363 } 364 365 // StartInstance is specified in the InstanceBroker interface. 366 func (env *azureEnviron) StartInstance(args environs.StartInstanceParams) (*environs.StartInstanceResult, error) { 367 if args.ControllerUUID == "" { 368 return nil, errors.New("missing controller UUID") 369 } 370 371 // Get the required configuration and config-dependent information 372 // required to create the instance. We take the lock just once, to 373 // ensure we obtain all information based on the same configuration. 374 env.mu.Lock() 375 envTags := tags.ResourceTags( 376 names.NewModelTag(env.config.Config.UUID()), 377 names.NewControllerTag(args.ControllerUUID), 378 env.config, 379 ) 380 storageAccountType := env.config.storageAccountType 381 imageStream := env.config.ImageStream() 382 instanceTypes, err := env.getInstanceTypesLocked() 383 if err != nil { 384 env.mu.Unlock() 385 return nil, errors.Trace(err) 386 } 387 env.mu.Unlock() 388 389 // If the user has not specified a root-disk size, then 390 // set a sensible default. 391 var rootDisk uint64 392 if args.Constraints.RootDisk != nil { 393 rootDisk = *args.Constraints.RootDisk 394 } else { 395 rootDisk = defaultRootDiskSize 396 args.Constraints.RootDisk = &rootDisk 397 } 398 399 // Identify the instance type and image to provision. 400 series := args.Tools.OneSeries() 401 instanceSpec, err := findInstanceSpec( 402 compute.VirtualMachineImagesClient{env.compute}, 403 instanceTypes, 404 &instances.InstanceConstraint{ 405 Region: env.location, 406 Series: series, 407 Arches: args.Tools.Arches(), 408 Constraints: args.Constraints, 409 }, 410 imageStream, 411 ) 412 if err != nil { 413 return nil, err 414 } 415 if rootDisk < uint64(instanceSpec.InstanceType.RootDisk) { 416 // The InstanceType's RootDisk is set to the maximum 417 // OS disk size; override it with the user-specified 418 // or default root disk size. 419 instanceSpec.InstanceType.RootDisk = rootDisk 420 } 421 422 // Windows images are 127GiB, and cannot be made smaller. 423 const windowsMinRootDiskMB = 127 * 1024 424 seriesOS, err := jujuseries.GetOSFromSeries(series) 425 if err != nil { 426 return nil, errors.Trace(err) 427 } 428 if seriesOS == os.Windows { 429 if instanceSpec.InstanceType.RootDisk < windowsMinRootDiskMB { 430 instanceSpec.InstanceType.RootDisk = windowsMinRootDiskMB 431 } 432 } 433 434 // Pick tools by filtering the available tools down to the architecture of 435 // the image that will be provisioned. 436 selectedTools, err := args.Tools.Match(tools.Filter{ 437 Arch: instanceSpec.Image.Arch, 438 }) 439 if err != nil { 440 return nil, errors.Trace(err) 441 } 442 logger.Infof("picked tools %q", selectedTools[0].Version) 443 444 // Finalize the instance config, which we'll render to CustomData below. 445 if err := args.InstanceConfig.SetTools(selectedTools); err != nil { 446 return nil, errors.Trace(err) 447 } 448 if err := instancecfg.FinishInstanceConfig( 449 args.InstanceConfig, env.Config(), 450 ); err != nil { 451 return nil, err 452 } 453 454 machineTag := names.NewMachineTag(args.InstanceConfig.MachineId) 455 vmName := resourceName(machineTag) 456 vmTags := make(map[string]string) 457 for k, v := range args.InstanceConfig.Tags { 458 vmTags[k] = v 459 } 460 // jujuMachineNameTag identifies the VM name, in which is encoded 461 // the Juju machine name. We tag all resources related to the 462 // machine with this. 463 vmTags[jujuMachineNameTag] = vmName 464 465 if err := env.createVirtualMachine( 466 vmName, vmTags, envTags, 467 instanceSpec, args.InstanceConfig, 468 storageAccountType, 469 ); err != nil { 470 logger.Errorf("creating instance failed, destroying: %v", err) 471 if err := env.StopInstances(instance.Id(vmName)); err != nil { 472 logger.Errorf("could not destroy failed virtual machine: %v", err) 473 } 474 return nil, errors.Annotatef(err, "creating virtual machine %q", vmName) 475 } 476 477 // Note: the instance is initialised without addresses to keep the 478 // API chatter down. We will refresh the instance if we need to know 479 // the addresses. 480 inst := &azureInstance{vmName, "Creating", env, nil, nil} 481 amd64 := arch.AMD64 482 hc := &instance.HardwareCharacteristics{ 483 Arch: &amd64, 484 Mem: &instanceSpec.InstanceType.Mem, 485 RootDisk: &instanceSpec.InstanceType.RootDisk, 486 CpuCores: &instanceSpec.InstanceType.CpuCores, 487 } 488 return &environs.StartInstanceResult{ 489 Instance: inst, 490 Hardware: hc, 491 }, nil 492 } 493 494 // createVirtualMachine creates a virtual machine and related resources. 495 // 496 // All resources created are tagged with the specified "vmTags", so if 497 // this function fails then all resources can be deleted by tag. 498 func (env *azureEnviron) createVirtualMachine( 499 vmName string, 500 vmTags, envTags map[string]string, 501 instanceSpec *instances.InstanceSpec, 502 instanceConfig *instancecfg.InstanceConfig, 503 storageAccountType string, 504 ) error { 505 506 deploymentsClient := resources.DeploymentsClient{env.resources} 507 508 var apiPort int 509 if instanceConfig.Controller != nil { 510 apiPortValue := instanceConfig.Controller.Config.APIPort() 511 apiPort = apiPortValue 512 } else { 513 apiPorts := instanceConfig.APIInfo.Ports() 514 if len(apiPorts) != 1 { 515 return errors.Errorf("expected one API port, found %v", apiPorts) 516 } 517 apiPort = apiPorts[0] 518 } 519 resources := networkTemplateResources(env.location, envTags, apiPort) 520 resources = append(resources, storageAccountTemplateResource( 521 env.location, envTags, 522 env.storageAccountName, storageAccountType, 523 )) 524 525 osProfile, seriesOS, err := newOSProfile( 526 vmName, instanceConfig, 527 env.provider.config.RandomWindowsAdminPassword, 528 ) 529 if err != nil { 530 return errors.Annotate(err, "creating OS profile") 531 } 532 storageProfile, err := newStorageProfile(vmName, env.storageAccountName, instanceSpec) 533 if err != nil { 534 return errors.Annotate(err, "creating storage profile") 535 } 536 537 var vmDependsOn []string 538 var availabilitySetSubResource *compute.SubResource 539 availabilitySetName, err := availabilitySetName( 540 vmName, vmTags, instanceConfig.Controller != nil, 541 ) 542 if err != nil { 543 return errors.Annotate(err, "getting availability set name") 544 } 545 if availabilitySetName != "" { 546 availabilitySetId := fmt.Sprintf( 547 `[resourceId('Microsoft.Compute/availabilitySets','%s')]`, 548 availabilitySetName, 549 ) 550 resources = append(resources, armtemplates.Resource{ 551 APIVersion: compute.APIVersion, 552 Type: "Microsoft.Compute/availabilitySets", 553 Name: availabilitySetName, 554 Location: env.location, 555 Tags: envTags, 556 }) 557 availabilitySetSubResource = &compute.SubResource{ 558 ID: to.StringPtr(availabilitySetId), 559 } 560 vmDependsOn = append(vmDependsOn, availabilitySetId) 561 } 562 563 publicIPAddressName := vmName + "-public-ip" 564 publicIPAddressId := fmt.Sprintf(`[resourceId('Microsoft.Network/publicIPAddresses', '%s')]`, publicIPAddressName) 565 resources = append(resources, armtemplates.Resource{ 566 APIVersion: network.APIVersion, 567 Type: "Microsoft.Network/publicIPAddresses", 568 Name: publicIPAddressName, 569 Location: env.location, 570 Tags: vmTags, 571 Properties: &network.PublicIPAddressPropertiesFormat{ 572 PublicIPAllocationMethod: network.Dynamic, 573 }, 574 }) 575 576 // Controller and non-controller machines are assigned to separate 577 // subnets. This enables us to create controller-specific NSG rules 578 // just by targeting the controller subnet. 579 subnetName := internalSubnetName 580 subnetPrefix := internalSubnetPrefix 581 if instanceConfig.Controller != nil { 582 subnetName = controllerSubnetName 583 subnetPrefix = controllerSubnetPrefix 584 } 585 subnetId := fmt.Sprintf( 586 `[concat(resourceId('Microsoft.Network/virtualNetworks', '%s'), '/subnets/%s')]`, 587 internalNetworkName, subnetName, 588 ) 589 590 privateIP, err := machineSubnetIP(subnetPrefix, instanceConfig.MachineId) 591 if err != nil { 592 return errors.Annotatef(err, "computing private IP address") 593 } 594 nicName := vmName + "-primary" 595 nicId := fmt.Sprintf(`[resourceId('Microsoft.Network/networkInterfaces', '%s')]`, nicName) 596 ipConfigurations := []network.InterfaceIPConfiguration{{ 597 Name: to.StringPtr("primary"), 598 Properties: &network.InterfaceIPConfigurationPropertiesFormat{ 599 Primary: to.BoolPtr(true), 600 PrivateIPAddress: to.StringPtr(privateIP.String()), 601 PrivateIPAllocationMethod: network.Static, 602 Subnet: &network.Subnet{ID: to.StringPtr(subnetId)}, 603 PublicIPAddress: &network.PublicIPAddress{ 604 ID: to.StringPtr(publicIPAddressId), 605 }, 606 }, 607 }} 608 resources = append(resources, armtemplates.Resource{ 609 APIVersion: network.APIVersion, 610 Type: "Microsoft.Network/networkInterfaces", 611 Name: nicName, 612 Location: env.location, 613 Tags: vmTags, 614 Properties: &network.InterfacePropertiesFormat{ 615 IPConfigurations: &ipConfigurations, 616 }, 617 DependsOn: []string{ 618 publicIPAddressId, 619 fmt.Sprintf( 620 `[resourceId('Microsoft.Network/virtualNetworks', '%s')]`, 621 internalNetworkName, 622 ), 623 }, 624 }) 625 626 nics := []compute.NetworkInterfaceReference{{ 627 ID: to.StringPtr(nicId), 628 Properties: &compute.NetworkInterfaceReferenceProperties{ 629 Primary: to.BoolPtr(true), 630 }, 631 }} 632 vmDependsOn = append(vmDependsOn, nicId) 633 vmDependsOn = append(vmDependsOn, fmt.Sprintf( 634 `[resourceId('Microsoft.Storage/storageAccounts', '%s')]`, 635 env.storageAccountName, 636 )) 637 resources = append(resources, armtemplates.Resource{ 638 APIVersion: compute.APIVersion, 639 Type: "Microsoft.Compute/virtualMachines", 640 Name: vmName, 641 Location: env.location, 642 Tags: vmTags, 643 Properties: &compute.VirtualMachineProperties{ 644 HardwareProfile: &compute.HardwareProfile{ 645 VMSize: compute.VirtualMachineSizeTypes( 646 instanceSpec.InstanceType.Name, 647 ), 648 }, 649 StorageProfile: storageProfile, 650 OsProfile: osProfile, 651 NetworkProfile: &compute.NetworkProfile{ 652 &nics, 653 }, 654 AvailabilitySet: availabilitySetSubResource, 655 }, 656 DependsOn: vmDependsOn, 657 }) 658 659 // On Windows and CentOS, we must add the CustomScript VM 660 // extension to run the CustomData script. 661 switch seriesOS { 662 case os.Windows, os.CentOS: 663 properties, err := vmExtensionProperties(seriesOS) 664 if err != nil { 665 return errors.Annotate( 666 err, "creating virtual machine extension", 667 ) 668 } 669 resources = append(resources, armtemplates.Resource{ 670 APIVersion: compute.APIVersion, 671 Type: "Microsoft.Compute/virtualMachines/extensions", 672 Name: vmName + "/" + extensionName, 673 Location: env.location, 674 Tags: vmTags, 675 Properties: properties, 676 DependsOn: []string{"Microsoft.Compute/virtualMachines/" + vmName}, 677 }) 678 } 679 680 logger.Debugf("- creating virtual machine deployment") 681 template := armtemplates.Template{Resources: resources} 682 // NOTE(axw) VMs take a long time to go to "Succeeded", so we do not 683 // block waiting for them to be fully provisioned. This means we won't 684 // return an error from StartInstance if the VM fails provisioning; 685 // we will instead report the error via the instance's status. 686 deploymentsClient.ResponseInspector = asyncCreationRespondDecorator( 687 deploymentsClient.ResponseInspector, 688 ) 689 if err := createDeployment( 690 env.callAPI, 691 deploymentsClient, 692 env.resourceGroup, 693 vmName, // deployment name 694 template, 695 ); err != nil { 696 return errors.Trace(err) 697 } 698 return nil 699 } 700 701 // createAvailabilitySet creates the availability set for a machine to use 702 // if it doesn't already exist, and returns the availability set's ID. The 703 // algorithm used for choosing the availability set is: 704 // - if the machine is a controller, use the availability set name 705 // "juju-controller"; 706 // - if the machine has units assigned, create an availability 707 // name with a name based on the value of the tags.JujuUnitsDeployed tag 708 // in vmTags, if it exists; 709 // - otherwise, do not assign the machine to an availability set 710 func availabilitySetName( 711 vmName string, 712 vmTags map[string]string, 713 controller bool, 714 ) (string, error) { 715 logger.Debugf("selecting availability set for %q", vmName) 716 if controller { 717 return controllerAvailabilitySet, nil 718 } 719 720 // We'll have to create an availability set. Use the name of one of the 721 // services assigned to the machine. 722 var availabilitySetName string 723 if unitNames, ok := vmTags[tags.JujuUnitsDeployed]; ok { 724 for _, unitName := range strings.Fields(unitNames) { 725 if !names.IsValidUnit(unitName) { 726 continue 727 } 728 serviceName, err := names.UnitApplication(unitName) 729 if err != nil { 730 return "", errors.Annotate(err, "getting service name") 731 } 732 availabilitySetName = serviceName 733 break 734 } 735 } 736 return availabilitySetName, nil 737 } 738 739 // newStorageProfile creates the storage profile for a virtual machine, 740 // based on the series and chosen instance spec. 741 func newStorageProfile( 742 vmName string, 743 storageAccountName string, 744 instanceSpec *instances.InstanceSpec, 745 ) (*compute.StorageProfile, error) { 746 logger.Debugf("creating storage profile for %q", vmName) 747 748 urnParts := strings.SplitN(instanceSpec.Image.Id, ":", 4) 749 if len(urnParts) != 4 { 750 return nil, errors.Errorf("invalid image ID %q", instanceSpec.Image.Id) 751 } 752 publisher := urnParts[0] 753 offer := urnParts[1] 754 sku := urnParts[2] 755 version := urnParts[3] 756 757 osDisksRoot := fmt.Sprintf( 758 `reference(resourceId('Microsoft.Storage/storageAccounts', '%s'), '%s').primaryEndpoints.blob`, 759 storageAccountName, storage.APIVersion, 760 ) 761 osDiskName := vmName 762 osDiskURI := fmt.Sprintf( 763 `[concat(%s, '%s/%s%s')]`, 764 osDisksRoot, osDiskVHDContainer, osDiskName, vhdExtension, 765 ) 766 osDiskSizeGB := mibToGB(instanceSpec.InstanceType.RootDisk) 767 osDisk := &compute.OSDisk{ 768 Name: to.StringPtr(osDiskName), 769 CreateOption: compute.FromImage, 770 Caching: compute.ReadWrite, 771 Vhd: &compute.VirtualHardDisk{URI: to.StringPtr(osDiskURI)}, 772 DiskSizeGB: to.Int32Ptr(int32(osDiskSizeGB)), 773 } 774 return &compute.StorageProfile{ 775 ImageReference: &compute.ImageReference{ 776 Publisher: to.StringPtr(publisher), 777 Offer: to.StringPtr(offer), 778 Sku: to.StringPtr(sku), 779 Version: to.StringPtr(version), 780 }, 781 OsDisk: osDisk, 782 }, nil 783 } 784 785 func mibToGB(mib uint64) uint64 { 786 b := float64(mib * 1024 * 1024) 787 return uint64(b / (1000 * 1000 * 1000)) 788 } 789 790 func newOSProfile( 791 vmName string, 792 instanceConfig *instancecfg.InstanceConfig, 793 randomAdminPassword func() string, 794 ) (*compute.OSProfile, os.OSType, error) { 795 logger.Debugf("creating OS profile for %q", vmName) 796 797 customData, err := providerinit.ComposeUserData(instanceConfig, nil, AzureRenderer{}) 798 if err != nil { 799 return nil, os.Unknown, errors.Annotate(err, "composing user data") 800 } 801 802 osProfile := &compute.OSProfile{ 803 ComputerName: to.StringPtr(vmName), 804 CustomData: to.StringPtr(string(customData)), 805 } 806 807 seriesOS, err := jujuseries.GetOSFromSeries(instanceConfig.Series) 808 if err != nil { 809 return nil, os.Unknown, errors.Trace(err) 810 } 811 switch seriesOS { 812 case os.Ubuntu, os.CentOS: 813 // SSH keys are handled by custom data, but must also be 814 // specified in order to forego providing a password, and 815 // disable password authentication. 816 publicKeys := []compute.SSHPublicKey{{ 817 Path: to.StringPtr("/home/ubuntu/.ssh/authorized_keys"), 818 KeyData: to.StringPtr(instanceConfig.AuthorizedKeys), 819 }} 820 osProfile.AdminUsername = to.StringPtr("ubuntu") 821 osProfile.LinuxConfiguration = &compute.LinuxConfiguration{ 822 DisablePasswordAuthentication: to.BoolPtr(true), 823 SSH: &compute.SSHConfiguration{PublicKeys: &publicKeys}, 824 } 825 case os.Windows: 826 osProfile.AdminUsername = to.StringPtr("JujuAdministrator") 827 // A password is required by Azure, but we will never use it. 828 // We generate something sufficiently long and random that it 829 // should be infeasible to guess. 830 osProfile.AdminPassword = to.StringPtr(randomAdminPassword()) 831 osProfile.WindowsConfiguration = &compute.WindowsConfiguration{ 832 ProvisionVMAgent: to.BoolPtr(true), 833 EnableAutomaticUpdates: to.BoolPtr(true), 834 // TODO(?) add WinRM configuration here. 835 } 836 default: 837 return nil, os.Unknown, errors.NotSupportedf("%s", seriesOS) 838 } 839 return osProfile, seriesOS, nil 840 } 841 842 // StopInstances is specified in the InstanceBroker interface. 843 func (env *azureEnviron) StopInstances(ids ...instance.Id) error { 844 if len(ids) == 0 { 845 return nil 846 } 847 848 // First up, cancel the deployments. Then we can identify the resources 849 // that need to be deleted without racing with their creation. 850 var wg sync.WaitGroup 851 var existing int 852 cancelResults := make([]error, len(ids)) 853 for i, id := range ids { 854 logger.Debugf("canceling deployment for instance %q", id) 855 wg.Add(1) 856 go func(i int, id instance.Id) { 857 defer wg.Done() 858 cancelResults[i] = errors.Annotatef( 859 env.cancelDeployment(string(id)), 860 "canceling deployment %q", id, 861 ) 862 }(i, id) 863 } 864 wg.Wait() 865 for _, err := range cancelResults { 866 if err == nil { 867 existing++ 868 } else if !errors.IsNotFound(err) { 869 return err 870 } 871 } 872 if existing == 0 { 873 // None of the instances exist, so we can stop now. 874 return nil 875 } 876 877 maybeStorageClient, err := env.getStorageClient() 878 if errors.IsNotFound(err) { 879 // It is possible, if unlikely, that the first deployment for a 880 // hosted model will fail or be canceled before the model's 881 // storage account is created. We must therefore cater for the 882 // account being missing or incomplete here. 883 maybeStorageClient = nil 884 } else if err != nil { 885 return errors.Trace(err) 886 } 887 888 // List network interfaces and public IP addresses. 889 instanceNics, err := instanceNetworkInterfaces( 890 env.callAPI, env.resourceGroup, 891 network.InterfacesClient{env.network}, 892 ) 893 if err != nil { 894 return errors.Trace(err) 895 } 896 instancePips, err := instancePublicIPAddresses( 897 env.callAPI, env.resourceGroup, 898 network.PublicIPAddressesClient{env.network}, 899 ) 900 if err != nil { 901 return errors.Trace(err) 902 } 903 904 // Delete the deployments, virtual machines, and related resources. 905 deleteResults := make([]error, len(ids)) 906 for i, id := range ids { 907 if errors.IsNotFound(cancelResults[i]) { 908 continue 909 } 910 // The deployment does not exist, so there's nothing more to do. 911 logger.Debugf("deleting instance %q", id) 912 wg.Add(1) 913 go func(i int, id instance.Id) { 914 defer wg.Done() 915 err := env.deleteVirtualMachine( 916 id, 917 maybeStorageClient, 918 instanceNics[id], 919 instancePips[id], 920 ) 921 deleteResults[i] = errors.Annotatef( 922 err, "deleting instance %q", id, 923 ) 924 }(i, id) 925 } 926 wg.Wait() 927 for _, err := range deleteResults { 928 if err != nil && !errors.IsNotFound(err) { 929 return errors.Trace(err) 930 } 931 } 932 933 return nil 934 } 935 936 // cancelDeployment cancels a template deployment. 937 func (env *azureEnviron) cancelDeployment(name string) error { 938 deploymentsClient := resources.DeploymentsClient{env.resources} 939 logger.Debugf("- canceling deployment %q", name) 940 var cancelResult autorest.Response 941 if err := env.callAPI(func() (autorest.Response, error) { 942 var err error 943 cancelResult, err = deploymentsClient.Cancel(env.resourceGroup, name) 944 return cancelResult, err 945 }); err != nil { 946 if cancelResult.Response != nil { 947 switch cancelResult.StatusCode { 948 case http.StatusNotFound: 949 return errors.NewNotFound(err, fmt.Sprintf("deployment %q not found", name)) 950 case http.StatusConflict: 951 if err, ok := errorutils.ServiceError(err); ok { 952 if err.Code == serviceErrorCodeDeploymentCannotBeCancelled { 953 // Deployments can only canceled while they're running. 954 return nil 955 } 956 } 957 } 958 } 959 return errors.Annotatef(err, "canceling deployment %q", name) 960 } 961 return nil 962 } 963 964 // deleteVirtualMachine deletes a virtual machine and all of the resources that 965 // it owns, and any corresponding network security rules. 966 func (env *azureEnviron) deleteVirtualMachine( 967 instId instance.Id, 968 maybeStorageClient internalazurestorage.Client, 969 networkInterfaces []network.Interface, 970 publicIPAddresses []network.PublicIPAddress, 971 ) error { 972 vmClient := compute.VirtualMachinesClient{env.compute} 973 nicClient := network.InterfacesClient{env.network} 974 nsgClient := network.SecurityGroupsClient{env.network} 975 securityRuleClient := network.SecurityRulesClient{env.network} 976 pipClient := network.PublicIPAddressesClient{env.network} 977 deploymentsClient := resources.DeploymentsClient{env.resources} 978 vmName := string(instId) 979 980 logger.Debugf("- deleting virtual machine (%s)", vmName) 981 if err := deleteResource(env.callAPI, vmClient, env.resourceGroup, vmName); err != nil { 982 if !errors.IsNotFound(err) { 983 return errors.Annotate(err, "deleting virtual machine") 984 } 985 } 986 987 if maybeStorageClient != nil { 988 logger.Debugf("- deleting OS VHD (%s)", vmName) 989 blobClient := maybeStorageClient.GetBlobService() 990 if _, err := blobClient.DeleteBlobIfExists(osDiskVHDContainer, vmName, nil); err != nil { 991 return errors.Annotate(err, "deleting OS VHD") 992 } 993 } 994 995 logger.Debugf("- deleting security rules (%s)", vmName) 996 if err := deleteInstanceNetworkSecurityRules( 997 env.resourceGroup, instId, nsgClient, 998 securityRuleClient, env.callAPI, 999 ); err != nil { 1000 return errors.Annotate(err, "deleting network security rules") 1001 } 1002 1003 logger.Debugf("- deleting network interfaces (%s)", vmName) 1004 for _, nic := range networkInterfaces { 1005 nicName := to.String(nic.Name) 1006 logger.Tracef("deleting NIC %q", nicName) 1007 if err := deleteResource(env.callAPI, nicClient, env.resourceGroup, nicName); err != nil { 1008 if !errors.IsNotFound(err) { 1009 return errors.Annotate(err, "deleting NIC") 1010 } 1011 } 1012 } 1013 1014 logger.Debugf("- deleting public IPs (%s)", vmName) 1015 for _, pip := range publicIPAddresses { 1016 pipName := to.String(pip.Name) 1017 logger.Tracef("deleting public IP %q", pipName) 1018 if err := deleteResource(env.callAPI, pipClient, env.resourceGroup, pipName); err != nil { 1019 if !errors.IsNotFound(err) { 1020 return errors.Annotate(err, "deleting public IP") 1021 } 1022 } 1023 } 1024 1025 // The deployment must be deleted last, or we risk leaking resources. 1026 logger.Debugf("- deleting deployment (%s)", vmName) 1027 if err := deleteResource(env.callAPI, deploymentsClient, env.resourceGroup, vmName); err != nil { 1028 if !errors.IsNotFound(err) { 1029 return errors.Annotate(err, "deleting deployment") 1030 } 1031 } 1032 return nil 1033 } 1034 1035 // Instances is specified in the Environ interface. 1036 func (env *azureEnviron) Instances(ids []instance.Id) ([]instance.Instance, error) { 1037 return env.instances(env.resourceGroup, ids, true /* refresh addresses */) 1038 } 1039 1040 func (env *azureEnviron) instances( 1041 resourceGroup string, 1042 ids []instance.Id, 1043 refreshAddresses bool, 1044 ) ([]instance.Instance, error) { 1045 if len(ids) == 0 { 1046 return nil, nil 1047 } 1048 all, err := env.allInstances(resourceGroup, refreshAddresses, false) 1049 if err != nil { 1050 return nil, errors.Trace(err) 1051 } 1052 byId := make(map[instance.Id]instance.Instance) 1053 for _, inst := range all { 1054 byId[inst.Id()] = inst 1055 } 1056 var found int 1057 matching := make([]instance.Instance, len(ids)) 1058 for i, id := range ids { 1059 inst, ok := byId[id] 1060 if !ok { 1061 continue 1062 } 1063 matching[i] = inst 1064 found++ 1065 } 1066 if found == 0 { 1067 return nil, environs.ErrNoInstances 1068 } else if found < len(ids) { 1069 return matching, environs.ErrPartialInstances 1070 } 1071 return matching, nil 1072 } 1073 1074 // AllInstances is specified in the InstanceBroker interface. 1075 func (env *azureEnviron) AllInstances() ([]instance.Instance, error) { 1076 return env.allInstances(env.resourceGroup, true /* refresh addresses */, false /* all instances */) 1077 } 1078 1079 // allInstances returns all of the instances in the given resource group, 1080 // and optionally ensures that each instance's addresses are up-to-date. 1081 func (env *azureEnviron) allInstances( 1082 resourceGroup string, 1083 refreshAddresses bool, 1084 controllerOnly bool, 1085 ) ([]instance.Instance, error) { 1086 deploymentsClient := resources.DeploymentsClient{env.resources} 1087 var deploymentsResult resources.DeploymentListResult 1088 if err := env.callAPI(func() (autorest.Response, error) { 1089 var err error 1090 deploymentsResult, err = deploymentsClient.List(resourceGroup, "", nil) 1091 return deploymentsResult.Response, err 1092 }); err != nil { 1093 if deploymentsResult.Response.Response != nil && deploymentsResult.StatusCode == http.StatusNotFound { 1094 // This will occur if the resource group does not 1095 // exist, e.g. in a fresh hosted environment. 1096 return nil, nil 1097 } 1098 return nil, errors.Trace(err) 1099 } 1100 if deploymentsResult.Value == nil || len(*deploymentsResult.Value) == 0 { 1101 return nil, nil 1102 } 1103 1104 azureInstances := make([]*azureInstance, 0, len(*deploymentsResult.Value)) 1105 for _, deployment := range *deploymentsResult.Value { 1106 name := to.String(deployment.Name) 1107 if deployment.Properties == nil || deployment.Properties.Dependencies == nil { 1108 continue 1109 } 1110 if controllerOnly && !isControllerDeployment(deployment) { 1111 continue 1112 } 1113 provisioningState := to.String(deployment.Properties.ProvisioningState) 1114 inst := &azureInstance{name, provisioningState, env, nil, nil} 1115 azureInstances = append(azureInstances, inst) 1116 } 1117 1118 if len(azureInstances) > 0 && refreshAddresses { 1119 if err := setInstanceAddresses( 1120 env.callAPI, 1121 resourceGroup, 1122 network.InterfacesClient{env.network}, 1123 network.PublicIPAddressesClient{env.network}, 1124 azureInstances, 1125 ); err != nil { 1126 return nil, errors.Trace(err) 1127 } 1128 } 1129 1130 instances := make([]instance.Instance, len(azureInstances)) 1131 for i, inst := range azureInstances { 1132 instances[i] = inst 1133 } 1134 return instances, nil 1135 } 1136 1137 func isControllerDeployment(deployment resources.DeploymentExtended) bool { 1138 for _, d := range *deployment.Properties.Dependencies { 1139 if d.DependsOn == nil { 1140 continue 1141 } 1142 if to.String(d.ResourceType) != "Microsoft.Compute/virtualMachines" { 1143 continue 1144 } 1145 for _, on := range *d.DependsOn { 1146 if to.String(on.ResourceType) != "Microsoft.Compute/availabilitySets" { 1147 continue 1148 } 1149 if to.String(on.ResourceName) == controllerAvailabilitySet { 1150 return true 1151 } 1152 } 1153 } 1154 return false 1155 } 1156 1157 // Destroy is specified in the Environ interface. 1158 func (env *azureEnviron) Destroy() error { 1159 logger.Debugf("destroying model %q", env.envName) 1160 logger.Debugf("- deleting resource group %q", env.resourceGroup) 1161 if err := env.deleteResourceGroup(env.resourceGroup); err != nil { 1162 return errors.Trace(err) 1163 } 1164 // Resource groups are self-contained and fully encompass 1165 // all environ resources. Once you delete the group, there 1166 // is nothing else to do. 1167 return nil 1168 } 1169 1170 // DestroyController is specified in the Environ interface. 1171 func (env *azureEnviron) DestroyController(controllerUUID string) error { 1172 logger.Debugf("destroying model %q", env.envName) 1173 logger.Debugf("- deleting resource groups") 1174 if err := env.deleteControllerManagedResourceGroups(controllerUUID); err != nil { 1175 return errors.Trace(err) 1176 } 1177 // Resource groups are self-contained and fully encompass 1178 // all environ resources. Once you delete the group, there 1179 // is nothing else to do. 1180 return nil 1181 } 1182 1183 func (env *azureEnviron) deleteControllerManagedResourceGroups(controllerUUID string) error { 1184 filter := fmt.Sprintf( 1185 "tagname eq '%s' and tagvalue eq '%s'", 1186 tags.JujuController, controllerUUID, 1187 ) 1188 client := resources.GroupsClient{env.resources} 1189 var result resources.ResourceGroupListResult 1190 if err := env.callAPI(func() (autorest.Response, error) { 1191 var err error 1192 result, err = client.List(filter, nil) 1193 return result.Response, err 1194 }); err != nil { 1195 return errors.Annotate(err, "listing resource groups") 1196 } 1197 if result.Value == nil { 1198 return nil 1199 } 1200 1201 // Deleting groups can take a long time, so make sure they are 1202 // deleted in parallel. 1203 var wg sync.WaitGroup 1204 errs := make([]error, len(*result.Value)) 1205 for i, group := range *result.Value { 1206 groupName := to.String(group.Name) 1207 logger.Debugf(" - deleting resource group %q", groupName) 1208 wg.Add(1) 1209 go func(i int) { 1210 defer wg.Done() 1211 if err := env.deleteResourceGroup(groupName); err != nil { 1212 errs[i] = errors.Annotatef( 1213 err, "deleting resource group %q", groupName, 1214 ) 1215 } 1216 }(i) 1217 } 1218 wg.Wait() 1219 1220 // If there is just one error, return it. If there are multiple, 1221 // then combine their messages. 1222 var nonNilErrs []error 1223 for _, err := range errs { 1224 if err != nil { 1225 nonNilErrs = append(nonNilErrs, err) 1226 } 1227 } 1228 switch len(nonNilErrs) { 1229 case 0: 1230 return nil 1231 case 1: 1232 return nonNilErrs[0] 1233 } 1234 combined := make([]string, len(nonNilErrs)) 1235 for i, err := range nonNilErrs { 1236 combined[i] = err.Error() 1237 } 1238 return errors.New(strings.Join(combined, "; ")) 1239 } 1240 1241 func (env *azureEnviron) deleteResourceGroup(resourceGroup string) error { 1242 client := resources.GroupsClient{env.resources} 1243 var result autorest.Response 1244 if err := env.callAPI(func() (autorest.Response, error) { 1245 var err error 1246 result, err = client.Delete(resourceGroup, nil) 1247 return result, err 1248 }); err != nil { 1249 if result.Response == nil || result.StatusCode != http.StatusNotFound { 1250 return errors.Annotatef(err, "deleting resource group %q", resourceGroup) 1251 } 1252 } 1253 return nil 1254 } 1255 1256 var errNoFwGlobal = errors.New("global firewall mode is not supported") 1257 1258 // OpenPorts is specified in the Environ interface. However, Azure does not 1259 // support the global firewall mode. 1260 func (env *azureEnviron) OpenPorts(ports []jujunetwork.PortRange) error { 1261 return errNoFwGlobal 1262 } 1263 1264 // ClosePorts is specified in the Environ interface. However, Azure does not 1265 // support the global firewall mode. 1266 func (env *azureEnviron) ClosePorts(ports []jujunetwork.PortRange) error { 1267 return errNoFwGlobal 1268 } 1269 1270 // Ports is specified in the Environ interface. 1271 func (env *azureEnviron) Ports() ([]jujunetwork.PortRange, error) { 1272 return nil, errNoFwGlobal 1273 } 1274 1275 // Provider is specified in the Environ interface. 1276 func (env *azureEnviron) Provider() environs.EnvironProvider { 1277 return env.provider 1278 } 1279 1280 // resourceGroupName returns the name of the environment's resource group. 1281 func resourceGroupName(modelTag names.ModelTag, modelName string) string { 1282 return fmt.Sprintf("juju-%s-%s", modelName, resourceName(modelTag)) 1283 } 1284 1285 // resourceName returns the string to use for a resource's Name tag, 1286 // to help users identify Juju-managed resources in the Azure portal. 1287 // 1288 // Since resources are grouped under resource groups, we just use the 1289 // tag. 1290 func resourceName(tag names.Tag) string { 1291 return tag.String() 1292 } 1293 1294 // getInstanceTypes gets the instance types available for the configured 1295 // location, keyed by name. 1296 func (env *azureEnviron) getInstanceTypes() (map[string]instances.InstanceType, error) { 1297 env.mu.Lock() 1298 defer env.mu.Unlock() 1299 instanceTypes, err := env.getInstanceTypesLocked() 1300 if err != nil { 1301 return nil, errors.Annotate(err, "getting instance types") 1302 } 1303 return instanceTypes, nil 1304 } 1305 1306 // getInstanceTypesLocked returns the instance types for Azure, by listing the 1307 // role sizes available to the subscription. 1308 func (env *azureEnviron) getInstanceTypesLocked() (map[string]instances.InstanceType, error) { 1309 if env.instanceTypes != nil { 1310 return env.instanceTypes, nil 1311 } 1312 1313 location := env.location 1314 client := compute.VirtualMachineSizesClient{env.compute} 1315 1316 var result compute.VirtualMachineSizeListResult 1317 if err := env.callAPI(func() (autorest.Response, error) { 1318 var err error 1319 result, err = client.List(location) 1320 return result.Response, err 1321 }); err != nil { 1322 return nil, errors.Annotate(err, "listing VM sizes") 1323 } 1324 instanceTypes := make(map[string]instances.InstanceType) 1325 if result.Value != nil { 1326 for _, size := range *result.Value { 1327 instanceType := newInstanceType(size) 1328 instanceTypes[instanceType.Name] = instanceType 1329 // Create aliases for standard role sizes. 1330 if strings.HasPrefix(instanceType.Name, "Standard_") { 1331 instanceTypes[instanceType.Name[len("Standard_"):]] = instanceType 1332 } 1333 } 1334 } 1335 env.instanceTypes = instanceTypes 1336 return instanceTypes, nil 1337 } 1338 1339 // getStorageClient queries the storage account key, and uses it to construct 1340 // a new storage client. 1341 func (env *azureEnviron) getStorageClient() (internalazurestorage.Client, error) { 1342 env.mu.Lock() 1343 defer env.mu.Unlock() 1344 storageAccount, err := env.getStorageAccountLocked(false) 1345 if err != nil { 1346 return nil, errors.Annotate(err, "getting storage account") 1347 } 1348 storageAccountKey, err := env.getStorageAccountKeyLocked( 1349 to.String(storageAccount.Name), false, 1350 ) 1351 if err != nil { 1352 return nil, errors.Annotate(err, "getting storage account key") 1353 } 1354 client, err := getStorageClient( 1355 env.provider.config.NewStorageClient, 1356 env.storageEndpoint, 1357 storageAccount, 1358 storageAccountKey, 1359 ) 1360 if err != nil { 1361 return nil, errors.Annotate(err, "getting storage client") 1362 } 1363 return client, nil 1364 } 1365 1366 // getStorageAccount returns the storage account for this environment's 1367 // resource group. If refresh is true, cached details will be refreshed. 1368 func (env *azureEnviron) getStorageAccount(refresh bool) (*storage.Account, error) { 1369 env.mu.Lock() 1370 defer env.mu.Unlock() 1371 return env.getStorageAccountLocked(refresh) 1372 } 1373 1374 func (env *azureEnviron) getStorageAccountLocked(refresh bool) (*storage.Account, error) { 1375 if !refresh && env.storageAccount != nil { 1376 return env.storageAccount, nil 1377 } 1378 client := storage.AccountsClient{env.storage} 1379 var account storage.Account 1380 if err := env.callAPI(func() (autorest.Response, error) { 1381 var err error 1382 account, err = client.GetProperties(env.resourceGroup, env.storageAccountName) 1383 return account.Response, err 1384 }); err != nil { 1385 if account.Response.Response != nil && account.Response.StatusCode == http.StatusNotFound { 1386 return nil, errors.NewNotFound(err, fmt.Sprintf("storage account not found")) 1387 } 1388 return nil, errors.Annotate(err, "getting storage account") 1389 } 1390 env.storageAccount = &account 1391 return env.storageAccount, nil 1392 } 1393 1394 // getStorageAccountKeysLocked returns a storage account key for this 1395 // environment's storage account. If refresh is true, any cached key 1396 // will be refreshed. This method assumes that env.mu is held. 1397 func (env *azureEnviron) getStorageAccountKeyLocked(accountName string, refresh bool) (*storage.AccountKey, error) { 1398 if !refresh && env.storageAccountKey != nil { 1399 return env.storageAccountKey, nil 1400 } 1401 client := storage.AccountsClient{env.storage} 1402 key, err := getStorageAccountKey( 1403 env.callAPI, 1404 client, 1405 env.resourceGroup, 1406 accountName, 1407 ) 1408 if err != nil { 1409 return nil, errors.Trace(err) 1410 } 1411 env.storageAccountKey = key 1412 return key, nil 1413 } 1414 1415 // AgentMirror is specified in the tools.HasAgentMirror interface. 1416 // 1417 // TODO(axw) 2016-04-11 #1568715 1418 // When we have image simplestreams, we should rename this to "Region", 1419 // to implement simplestreams.HasRegion. 1420 func (env *azureEnviron) AgentMirror() (simplestreams.CloudSpec, error) { 1421 return simplestreams.CloudSpec{ 1422 Region: env.location, 1423 // The endpoints published in simplestreams 1424 // data are the storage endpoints. 1425 Endpoint: fmt.Sprintf("https://%s/", env.storageEndpoint), 1426 }, nil 1427 } 1428 1429 func (env *azureEnviron) callAPI(f func() (autorest.Response, error)) error { 1430 return backoffAPIRequestCaller{env.provider.config.RetryClock}.call(f) 1431 }