github.com/niedbalski/juju@v0.0.0-20190215020005-8ff100488e47/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 stdcontext "context" 8 "fmt" 9 "net/http" 10 "net/url" 11 "sort" 12 "strings" 13 "sync" 14 "time" 15 16 "github.com/Azure/azure-sdk-for-go/services/compute/mgmt/2018-10-01/compute" 17 "github.com/Azure/azure-sdk-for-go/services/network/mgmt/2018-08-01/network" 18 "github.com/Azure/azure-sdk-for-go/services/resources/mgmt/2018-05-01/resources" 19 "github.com/Azure/azure-sdk-for-go/services/storage/mgmt/2018-07-01/storage" 20 azurestorage "github.com/Azure/azure-sdk-for-go/storage" 21 "github.com/Azure/go-autorest/autorest" 22 "github.com/Azure/go-autorest/autorest/to" 23 "github.com/juju/errors" 24 "github.com/juju/loggo" 25 "github.com/juju/os" 26 jujuseries "github.com/juju/os/series" 27 "github.com/juju/retry" 28 "github.com/juju/utils/arch" 29 "github.com/juju/version" 30 "gopkg.in/juju/names.v2" 31 32 "github.com/juju/juju/cloudconfig/instancecfg" 33 "github.com/juju/juju/cloudconfig/providerinit" 34 "github.com/juju/juju/core/constraints" 35 "github.com/juju/juju/core/instance" 36 "github.com/juju/juju/environs" 37 "github.com/juju/juju/environs/config" 38 "github.com/juju/juju/environs/instances" 39 "github.com/juju/juju/environs/simplestreams" 40 "github.com/juju/juju/environs/tags" 41 42 "github.com/juju/juju/environs/context" 43 "github.com/juju/juju/provider/azure/internal/armtemplates" 44 internalazureresources "github.com/juju/juju/provider/azure/internal/azureresources" 45 internalazurestorage "github.com/juju/juju/provider/azure/internal/azurestorage" 46 "github.com/juju/juju/provider/azure/internal/errorutils" 47 "github.com/juju/juju/provider/azure/internal/tracing" 48 "github.com/juju/juju/provider/azure/internal/useragent" 49 "github.com/juju/juju/provider/common" 50 "github.com/juju/juju/tools" 51 ) 52 53 const ( 54 jujuMachineNameTag = tags.JujuTagPrefix + "machine-name" 55 56 // minRootDiskSize is the minimum root disk size Azure 57 // accepts for a VM's OS disk. 58 // It will be used if none is specified by the user. 59 minRootDiskSize = 30 * 1024 // 30 GiB 60 61 // serviceErrorCodeDeploymentCannotBeCancelled is the error code for 62 // service errors in response to an attempt to cancel a deployment 63 // that cannot be cancelled. 64 serviceErrorCodeDeploymentCannotBeCancelled = "DeploymentCannotBeCancelled" 65 66 // serviceErrorCodeResourceGroupBeingDeleted is the error code for 67 // service errors in response to an attempt to cancel a deployment 68 // that has already started to be deleted. 69 serviceErrorCodeResourceGroupBeingDeleted = "ResourceGroupBeingDeleted" 70 71 // controllerAvailabilitySet is the name of the availability set 72 // used for controller machines. 73 controllerAvailabilitySet = "juju-controller" 74 75 computeAPIVersion = "2018-10-01" 76 networkAPIVersion = "2018-08-01" 77 storageAPIVersion = "2018-07-01" 78 ) 79 80 type azureEnviron struct { 81 // provider is the azureEnvironProvider used to open this environment. 82 provider *azureEnvironProvider 83 84 // cloud defines the cloud configuration for this environment. 85 cloud environs.CloudSpec 86 87 // location is the canonicalized location name. Use this instead 88 // of cloud.Region in API calls. 89 location string 90 91 // subscriptionId is the Azure account subscription ID. 92 subscriptionId string 93 94 // storageEndpoint is the Azure storage endpoint. This is the host 95 // portion of the storage endpoint URL only; use this instead of 96 // cloud.StorageEndpoint in API calls. 97 storageEndpoint string 98 99 // resourceGroup is the name of the Resource Group in the Azure 100 // subscription that corresponds to the environment. 101 resourceGroup string 102 103 // envName is the name of the environment. 104 envName string 105 106 // authorizer is the authorizer we use for Azure. 107 authorizer *cloudSpecAuth 108 109 compute compute.BaseClient 110 disk compute.BaseClient 111 resources resources.BaseClient 112 storage storage.BaseClient 113 network network.BaseClient 114 storageClient azurestorage.Client 115 storageAccountName string 116 117 mu sync.Mutex 118 config *azureModelConfig 119 instanceTypes map[string]instances.InstanceType 120 storageAccount **storage.Account 121 storageAccountKey *storage.AccountKey 122 commonResourcesCreated bool 123 } 124 125 var _ environs.Environ = (*azureEnviron)(nil) 126 127 // newEnviron creates a new azureEnviron. 128 func newEnviron( 129 provider *azureEnvironProvider, 130 cloud environs.CloudSpec, 131 cfg *config.Config, 132 ) (*azureEnviron, error) { 133 134 // The Azure storage code wants the endpoint host only, not the URL. 135 storageEndpointURL, err := url.Parse(cloud.StorageEndpoint) 136 if err != nil { 137 return nil, errors.Annotate(err, "parsing storage endpoint URL") 138 } 139 140 env := azureEnviron{ 141 provider: provider, 142 cloud: cloud, 143 location: canonicalLocation(cloud.Region), 144 storageEndpoint: storageEndpointURL.Host, 145 } 146 if err := env.initEnviron(); err != nil { 147 return nil, errors.Trace(err) 148 } 149 150 if err := env.SetConfig(cfg); err != nil { 151 return nil, errors.Trace(err) 152 } 153 154 modelTag := names.NewModelTag(cfg.UUID()) 155 env.resourceGroup = resourceGroupName(modelTag, cfg.Name()) 156 env.envName = cfg.Name() 157 158 // We need a deterministic storage account name, so that we can 159 // defer creation of the storage account to the VM deployment, 160 // and retain the ability to create multiple deployments in 161 // parallel. 162 // 163 // We use the last 20 non-hyphen hex characters of the model's 164 // UUID as the storage account name, prefixed with "juju". The 165 // probability of clashing with another storage account should 166 // be negligible. 167 uuidAlphaNumeric := strings.Replace(env.config.Config.UUID(), "-", "", -1) 168 env.storageAccountName = "juju" + uuidAlphaNumeric[len(uuidAlphaNumeric)-20:] 169 170 return &env, nil 171 } 172 173 func (env *azureEnviron) initEnviron() error { 174 credAttrs := env.cloud.Credential.Attributes() 175 env.subscriptionId = credAttrs[credAttrSubscriptionId] 176 env.authorizer = &cloudSpecAuth{ 177 cloud: env.cloud, 178 sender: env.provider.config.Sender, 179 } 180 181 env.compute = compute.NewWithBaseURI(env.cloud.Endpoint, env.subscriptionId) 182 env.disk = compute.NewWithBaseURI(env.cloud.Endpoint, env.subscriptionId) 183 env.resources = resources.NewWithBaseURI(env.cloud.Endpoint, env.subscriptionId) 184 env.storage = storage.NewWithBaseURI(env.cloud.Endpoint, env.subscriptionId) 185 env.network = network.NewWithBaseURI(env.cloud.Endpoint, env.subscriptionId) 186 clients := map[string]*autorest.Client{ 187 "azure.compute": &env.compute.Client, 188 "azure.disk": &env.disk.Client, 189 "azure.resources": &env.resources.Client, 190 "azure.storage": &env.storage.Client, 191 "azure.network": &env.network.Client, 192 } 193 for id, client := range clients { 194 useragent.UpdateClient(client) 195 client.Authorizer = env.authorizer 196 logger := loggo.GetLogger(id) 197 if env.provider.config.Sender != nil { 198 client.Sender = env.provider.config.Sender 199 } 200 client.ResponseInspector = tracing.RespondDecorator(logger) 201 client.RequestInspector = tracing.PrepareDecorator(logger) 202 if env.provider.config.RequestInspector != nil { 203 tracer := client.RequestInspector 204 inspector := env.provider.config.RequestInspector 205 client.RequestInspector = func(p autorest.Preparer) autorest.Preparer { 206 p = tracer(p) 207 p = inspector(p) 208 return p 209 } 210 } 211 } 212 return nil 213 } 214 215 // PrepareForBootstrap is part of the Environ interface. 216 func (env *azureEnviron) PrepareForBootstrap(ctx environs.BootstrapContext) error { 217 if ctx.ShouldVerifyCredentials() { 218 if err := verifyCredentials(env, nil); err != nil { 219 return errors.Trace(err) 220 } 221 } 222 return nil 223 } 224 225 // Create is part of the Environ interface. 226 func (env *azureEnviron) Create(ctx context.ProviderCallContext, args environs.CreateParams) error { 227 if err := verifyCredentials(env, ctx); err != nil { 228 return errors.Trace(err) 229 } 230 return errors.Trace(env.initResourceGroup(ctx, args.ControllerUUID, false)) 231 } 232 233 // Bootstrap is part of the Environ interface. 234 func (env *azureEnviron) Bootstrap( 235 ctx environs.BootstrapContext, 236 callCtx context.ProviderCallContext, 237 args environs.BootstrapParams, 238 ) (*environs.BootstrapResult, error) { 239 if err := env.initResourceGroup(callCtx, args.ControllerConfig.ControllerUUID(), true); err != nil { 240 return nil, errors.Annotate(err, "creating controller resource group") 241 } 242 result, err := common.Bootstrap(ctx, env, callCtx, args) 243 if err != nil { 244 logger.Errorf("bootstrap failed, destroying model: %v", err) 245 if err := env.Destroy(callCtx); err != nil { 246 logger.Errorf("failed to destroy model: %v", err) 247 } 248 return nil, errors.Trace(err) 249 } 250 return result, nil 251 } 252 253 // initResourceGroup creates a resource group for this environment. 254 func (env *azureEnviron) initResourceGroup(ctx context.ProviderCallContext, controllerUUID string, controller bool) error { 255 resourceGroupsClient := resources.GroupsClient{env.resources} 256 257 env.mu.Lock() 258 tags := tags.ResourceTags( 259 names.NewModelTag(env.config.Config.UUID()), 260 names.NewControllerTag(controllerUUID), 261 env.config, 262 ) 263 env.mu.Unlock() 264 265 logger.Debugf("creating resource group %q", env.resourceGroup) 266 sdkCtx := stdcontext.Background() 267 if _, err := resourceGroupsClient.CreateOrUpdate(sdkCtx, env.resourceGroup, resources.Group{ 268 Location: to.StringPtr(env.location), 269 Tags: *to.StringMapPtr(tags), 270 }); err != nil { 271 return errorutils.HandleCredentialError(errors.Annotate(err, "creating resource group"), ctx) 272 } 273 274 if !controller { 275 // When we create a resource group for a non-controller model, 276 // we must create the common resources up-front. This is so 277 // that parallel deployments do not affect dynamic changes, 278 // e.g. those made by the firewaller. For the controller model, 279 // we fold the creation of these resources into the bootstrap 280 // machine's deployment. 281 if err := env.createCommonResourceDeployment(ctx, tags, nil); err != nil { 282 return errors.Trace(err) 283 } 284 } 285 286 // New models are not given a storage account. Initialise the 287 // storage account pointer to a pointer to a nil pointer, so 288 // "getStorageAccount" avoids making an API call. 289 env.storageAccount = new(*storage.Account) 290 291 return nil 292 } 293 294 func (env *azureEnviron) createCommonResourceDeployment( 295 ctx context.ProviderCallContext, 296 tags map[string]string, 297 rules []network.SecurityRule, 298 commonResources ...armtemplates.Resource, 299 ) error { 300 commonResources = append(commonResources, networkTemplateResources( 301 env.location, tags, nil, rules, 302 )...) 303 304 // We perform this deployment asynchronously, to avoid blocking 305 // the "juju add-model" command; Create is called synchronously. 306 // Eventually we should have Create called asynchronously, but 307 // until then we do this, and ensure that the deployment has 308 // completed before we schedule additional deployments. 309 deploymentsClient := resources.DeploymentsClient{env.resources} 310 deploymentsClient.ResponseInspector = asyncCreationRespondDecorator( 311 deploymentsClient.ResponseInspector, 312 ) 313 template := armtemplates.Template{Resources: commonResources} 314 if err := createDeployment( 315 ctx, 316 deploymentsClient, 317 env.resourceGroup, 318 "common", // deployment name 319 template, 320 ); err != nil { 321 return errors.Trace(err) 322 } 323 return nil 324 } 325 326 // ControllerInstances is specified in the Environ interface. 327 func (env *azureEnviron) ControllerInstances(ctx context.ProviderCallContext, controllerUUID string) ([]instance.Id, error) { 328 instances, err := env.allInstances(ctx, env.resourceGroup, false, true) 329 if err != nil { 330 return nil, err 331 } 332 if len(instances) == 0 { 333 return nil, environs.ErrNoInstances 334 } 335 ids := make([]instance.Id, len(instances)) 336 for i, inst := range instances { 337 ids[i] = inst.Id() 338 } 339 return ids, nil 340 } 341 342 // Config is specified in the Environ interface. 343 func (env *azureEnviron) Config() *config.Config { 344 env.mu.Lock() 345 defer env.mu.Unlock() 346 return env.config.Config 347 } 348 349 // SetConfig is specified in the Environ interface. 350 func (env *azureEnviron) SetConfig(cfg *config.Config) error { 351 env.mu.Lock() 352 defer env.mu.Unlock() 353 354 var old *config.Config 355 if env.config != nil { 356 old = env.config.Config 357 } 358 ecfg, err := validateConfig(cfg, old) 359 if err != nil { 360 return err 361 } 362 env.config = ecfg 363 364 return nil 365 } 366 367 // ConstraintsValidator is defined on the Environs interface. 368 func (env *azureEnviron) ConstraintsValidator(ctx context.ProviderCallContext) (constraints.Validator, error) { 369 instanceTypes, err := env.getInstanceTypes(ctx) 370 if err != nil { 371 return nil, err 372 } 373 instTypeNames := make([]string, 0, len(instanceTypes)) 374 for instTypeName := range instanceTypes { 375 instTypeNames = append(instTypeNames, instTypeName) 376 } 377 sort.Strings(instTypeNames) 378 379 validator := constraints.NewValidator() 380 validator.RegisterUnsupported([]string{ 381 constraints.CpuPower, 382 constraints.Tags, 383 constraints.VirtType, 384 }) 385 validator.RegisterVocabulary( 386 constraints.Arch, 387 []string{arch.AMD64}, 388 ) 389 validator.RegisterVocabulary( 390 constraints.InstanceType, 391 instTypeNames, 392 ) 393 validator.RegisterConflicts( 394 []string{constraints.InstanceType}, 395 []string{ 396 constraints.Mem, 397 constraints.Cores, 398 constraints.Arch, 399 }, 400 ) 401 return validator, nil 402 } 403 404 // PrecheckInstance is defined on the environs.InstancePrechecker interface. 405 func (env *azureEnviron) PrecheckInstance(ctx context.ProviderCallContext, args environs.PrecheckInstanceParams) error { 406 if args.Placement != "" { 407 return fmt.Errorf("unknown placement directive: %s", args.Placement) 408 } 409 if !args.Constraints.HasInstanceType() { 410 return nil 411 } 412 // Constraint has an instance-type constraint so let's see if it is valid. 413 instanceTypes, err := env.getInstanceTypes(ctx) 414 if err != nil { 415 return err 416 } 417 for _, instanceType := range instanceTypes { 418 if instanceType.Name == *args.Constraints.InstanceType { 419 return nil 420 } 421 } 422 return fmt.Errorf("invalid instance type %q", *args.Constraints.InstanceType) 423 } 424 425 // MaintainInstance is specified in the InstanceBroker interface. 426 func (*azureEnviron) MaintainInstance(ctx context.ProviderCallContext, args environs.StartInstanceParams) error { 427 return nil 428 } 429 430 // StartInstance is specified in the InstanceBroker interface. 431 func (env *azureEnviron) StartInstance(ctx context.ProviderCallContext, args environs.StartInstanceParams) (*environs.StartInstanceResult, error) { 432 if args.ControllerUUID == "" { 433 return nil, errors.New("missing controller UUID") 434 } 435 436 // Get the required configuration and config-dependent information 437 // required to create the instance. We take the lock just once, to 438 // ensure we obtain all information based on the same configuration. 439 env.mu.Lock() 440 envTags := tags.ResourceTags( 441 names.NewModelTag(env.config.Config.UUID()), 442 names.NewControllerTag(args.ControllerUUID), 443 env.config, 444 ) 445 storageAccountType := env.config.storageAccountType 446 imageStream := env.config.ImageStream() 447 instanceTypes, err := env.getInstanceTypesLocked(ctx) 448 if err != nil { 449 env.mu.Unlock() 450 return nil, errors.Trace(err) 451 } 452 env.mu.Unlock() 453 454 // If the user has not specified a root-disk size, then 455 // set a sensible default. 456 var rootDisk uint64 457 // Azure complains if we try and specify a root disk size less than the minimum. 458 // See http://pad.lv/1645408 459 if args.Constraints.RootDisk != nil && *args.Constraints.RootDisk > minRootDiskSize { 460 rootDisk = *args.Constraints.RootDisk 461 } else { 462 rootDisk = minRootDiskSize 463 args.Constraints.RootDisk = &rootDisk 464 } 465 466 // Identify the instance type and image to provision. 467 series := args.Tools.OneSeries() 468 instanceSpec, err := findInstanceSpec( 469 ctx, 470 compute.VirtualMachineImagesClient{env.compute}, 471 instanceTypes, 472 &instances.InstanceConstraint{ 473 Region: env.location, 474 Series: series, 475 Arches: args.Tools.Arches(), 476 Constraints: args.Constraints, 477 }, 478 imageStream, 479 ) 480 if err != nil { 481 return nil, err 482 } 483 if rootDisk < instanceSpec.InstanceType.RootDisk { 484 // The InstanceType's RootDisk is set to the maximum 485 // OS disk size; override it with the user-specified 486 // or default root disk size. 487 instanceSpec.InstanceType.RootDisk = rootDisk 488 } 489 490 // Windows images are 127GiB, and cannot be made smaller. 491 const windowsMinRootDiskMB = 127 * 1024 492 seriesOS, err := jujuseries.GetOSFromSeries(series) 493 if err != nil { 494 return nil, errors.Trace(err) 495 } 496 if seriesOS == os.Windows { 497 if instanceSpec.InstanceType.RootDisk < windowsMinRootDiskMB { 498 instanceSpec.InstanceType.RootDisk = windowsMinRootDiskMB 499 } 500 } 501 502 // Pick tools by filtering the available tools down to the architecture of 503 // the image that will be provisioned. 504 selectedTools, err := args.Tools.Match(tools.Filter{ 505 Arch: instanceSpec.Image.Arch, 506 }) 507 if err != nil { 508 return nil, errors.Trace(err) 509 } 510 logger.Infof("picked agent binaries %q", selectedTools[0].Version) 511 512 // Finalize the instance config, which we'll render to CustomData below. 513 if err := args.InstanceConfig.SetTools(selectedTools); err != nil { 514 return nil, errors.Trace(err) 515 } 516 if err := instancecfg.FinishInstanceConfig( 517 args.InstanceConfig, env.Config(), 518 ); err != nil { 519 return nil, err 520 } 521 522 machineTag := names.NewMachineTag(args.InstanceConfig.MachineId) 523 vmName := resourceName(machineTag) 524 vmTags := make(map[string]string) 525 for k, v := range args.InstanceConfig.Tags { 526 vmTags[k] = v 527 } 528 // jujuMachineNameTag identifies the VM name, in which is encoded 529 // the Juju machine name. We tag all resources related to the 530 // machine with this. 531 vmTags[jujuMachineNameTag] = vmName 532 533 if err := env.createVirtualMachine( 534 ctx, vmName, vmTags, envTags, 535 instanceSpec, args.InstanceConfig, 536 storageAccountType, 537 ); err != nil { 538 logger.Errorf("creating instance failed, destroying: %v", err) 539 if err := env.StopInstances(ctx, instance.Id(vmName)); err != nil { 540 logger.Errorf("could not destroy failed virtual machine: %v", err) 541 } 542 return nil, errors.Annotatef(err, "creating virtual machine %q", vmName) 543 } 544 545 // Note: the instance is initialised without addresses to keep the 546 // API chatter down. We will refresh the instance if we need to know 547 // the addresses. 548 inst := &azureInstance{vmName, "Creating", env, nil, nil} 549 amd64 := arch.AMD64 550 hc := &instance.HardwareCharacteristics{ 551 Arch: &amd64, 552 Mem: &instanceSpec.InstanceType.Mem, 553 RootDisk: &instanceSpec.InstanceType.RootDisk, 554 CpuCores: &instanceSpec.InstanceType.CpuCores, 555 } 556 return &environs.StartInstanceResult{ 557 Instance: inst, 558 Hardware: hc, 559 }, nil 560 } 561 562 // createVirtualMachine creates a virtual machine and related resources. 563 // 564 // All resources created are tagged with the specified "vmTags", so if 565 // this function fails then all resources can be deleted by tag. 566 func (env *azureEnviron) createVirtualMachine( 567 ctx context.ProviderCallContext, 568 vmName string, 569 vmTags, envTags map[string]string, 570 instanceSpec *instances.InstanceSpec, 571 instanceConfig *instancecfg.InstanceConfig, 572 storageAccountType string, 573 ) error { 574 deploymentsClient := resources.DeploymentsClient{ 575 BaseClient: env.resources, 576 } 577 apiPorts := make([]int, 0, 2) 578 if instanceConfig.Controller != nil { 579 apiPorts = append(apiPorts, instanceConfig.Controller.Config.APIPort()) 580 if instanceConfig.Controller.Config.AutocertDNSName() != "" { 581 // Open port 80 as well as it handles Let's Encrypt HTTP challenge. 582 apiPorts = append(apiPorts, 80) 583 } 584 } else { 585 ports := instanceConfig.APIInfo.Ports() 586 if len(ports) != 1 { 587 return errors.Errorf("expected one API port, found %v", ports) 588 } 589 apiPorts = append(apiPorts, ports[0]) 590 } 591 592 var nicDependsOn, vmDependsOn []string 593 var resources []armtemplates.Resource 594 createCommonResources := instanceConfig.Bootstrap != nil 595 if createCommonResources { 596 // We're starting the bootstrap machine, so we will create the 597 // common resources in the same deployment. 598 resources = append(resources, 599 networkTemplateResources(env.location, envTags, apiPorts, nil)..., 600 ) 601 nicDependsOn = append(nicDependsOn, fmt.Sprintf( 602 `[resourceId('Microsoft.Network/virtualNetworks', '%s')]`, 603 internalNetworkName, 604 )) 605 } else { 606 // Wait for the common resource deployment to complete. 607 if err := env.waitCommonResourcesCreated(); err != nil { 608 return errors.Annotate( 609 err, "waiting for common resources to be created", 610 ) 611 } 612 } 613 614 maybeStorageAccount, err := env.getStorageAccount() 615 if errors.IsNotFound(err) { 616 // Only models created prior to Juju 2.3 will have a storage 617 // account. Juju 2.3 onwards exclusively uses managed disks 618 // for all new models, and handles both managed and unmanaged 619 // disks for upgraded models. 620 maybeStorageAccount = nil 621 } else if err != nil { 622 return errors.Trace(err) 623 } 624 625 osProfile, seriesOS, err := newOSProfile( 626 vmName, instanceConfig, 627 env.provider.config.RandomWindowsAdminPassword, 628 env.provider.config.GenerateSSHKey, 629 ) 630 if err != nil { 631 return errors.Annotate(err, "creating OS profile") 632 } 633 storageProfile, err := newStorageProfile( 634 vmName, 635 maybeStorageAccount, 636 storageAccountType, 637 instanceSpec, 638 ) 639 if err != nil { 640 return errors.Annotate(err, "creating storage profile") 641 } 642 643 var availabilitySetSubResource *compute.SubResource 644 availabilitySetName, err := availabilitySetName( 645 vmName, vmTags, instanceConfig.Controller != nil, 646 ) 647 if err != nil { 648 return errors.Annotate(err, "getting availability set name") 649 } 650 if availabilitySetName != "" { 651 availabilitySetId := fmt.Sprintf( 652 `[resourceId('Microsoft.Compute/availabilitySets','%s')]`, 653 availabilitySetName, 654 ) 655 var ( 656 availabilitySetProperties interface{} 657 availabilityStorageOptions *storage.Sku 658 ) 659 if maybeStorageAccount == nil { 660 // This model uses managed disks; we must create 661 // the availability set as "aligned" to support 662 // them. 663 availabilitySetProperties = &compute.AvailabilitySetProperties{ 664 // Azure complains when the fault domain count 665 // is not specified, even though it is meant 666 // to be optional and default to the maximum. 667 // The maximum depends on the location, and 668 // there is no API to query it. 669 PlatformFaultDomainCount: to.Int32Ptr(maxFaultDomains(env.location)), 670 } 671 // Availability needs to be 'Aligned' to support managed disks. 672 availabilityStorageOptions = &storage.Sku{ 673 Name: "Aligned", 674 } 675 } 676 resources = append(resources, armtemplates.Resource{ 677 APIVersion: computeAPIVersion, 678 Type: "Microsoft.Compute/availabilitySets", 679 Name: availabilitySetName, 680 Location: env.location, 681 Tags: envTags, 682 Properties: availabilitySetProperties, 683 StorageSku: availabilityStorageOptions, 684 }) 685 availabilitySetSubResource = &compute.SubResource{ 686 ID: to.StringPtr(availabilitySetId), 687 } 688 vmDependsOn = append(vmDependsOn, availabilitySetId) 689 } 690 691 publicIPAddressName := vmName + "-public-ip" 692 publicIPAddressId := fmt.Sprintf(`[resourceId('Microsoft.Network/publicIPAddresses', '%s')]`, publicIPAddressName) 693 resources = append(resources, armtemplates.Resource{ 694 APIVersion: networkAPIVersion, 695 Type: "Microsoft.Network/publicIPAddresses", 696 Name: publicIPAddressName, 697 Location: env.location, 698 Tags: vmTags, 699 StorageSku: &storage.Sku{Name: "Standard", Tier: "Regional"}, 700 Properties: &network.PublicIPAddressPropertiesFormat{ 701 PublicIPAddressVersion: network.IPv4, 702 PublicIPAllocationMethod: network.Static, 703 }, 704 }) 705 706 // Controller and non-controller machines are assigned to separate 707 // subnets. This enables us to create controller-specific NSG rules 708 // just by targeting the controller subnet. 709 subnetName := internalSubnetName 710 subnetPrefix := internalSubnetPrefix 711 if instanceConfig.Controller != nil { 712 subnetName = controllerSubnetName 713 subnetPrefix = controllerSubnetPrefix 714 } 715 subnetId := fmt.Sprintf( 716 `[concat(resourceId('Microsoft.Network/virtualNetworks', '%s'), '/subnets/%s')]`, 717 internalNetworkName, subnetName, 718 ) 719 720 privateIP, err := machineSubnetIP(subnetPrefix, instanceConfig.MachineId) 721 if err != nil { 722 return errors.Annotatef(err, "computing private IP address") 723 } 724 nicName := vmName + "-primary" 725 nicId := fmt.Sprintf(`[resourceId('Microsoft.Network/networkInterfaces', '%s')]`, nicName) 726 nicDependsOn = append(nicDependsOn, publicIPAddressId) 727 ipConfigurations := []network.InterfaceIPConfiguration{{ 728 Name: to.StringPtr("primary"), 729 InterfaceIPConfigurationPropertiesFormat: &network.InterfaceIPConfigurationPropertiesFormat{ 730 Primary: to.BoolPtr(true), 731 PrivateIPAddress: to.StringPtr(privateIP.String()), 732 PrivateIPAllocationMethod: network.Static, 733 Subnet: &network.Subnet{ID: to.StringPtr(subnetId)}, 734 PublicIPAddress: &network.PublicIPAddress{ 735 ID: to.StringPtr(publicIPAddressId), 736 }, 737 }, 738 }} 739 resources = append(resources, armtemplates.Resource{ 740 APIVersion: networkAPIVersion, 741 Type: "Microsoft.Network/networkInterfaces", 742 Name: nicName, 743 Location: env.location, 744 Tags: vmTags, 745 Properties: &network.InterfacePropertiesFormat{ 746 IPConfigurations: &ipConfigurations, 747 }, 748 DependsOn: nicDependsOn, 749 }) 750 751 nics := []compute.NetworkInterfaceReference{{ 752 ID: to.StringPtr(nicId), 753 NetworkInterfaceReferenceProperties: &compute.NetworkInterfaceReferenceProperties{ 754 Primary: to.BoolPtr(true), 755 }, 756 }} 757 vmDependsOn = append(vmDependsOn, nicId) 758 resources = append(resources, armtemplates.Resource{ 759 APIVersion: computeAPIVersion, 760 Type: "Microsoft.Compute/virtualMachines", 761 Name: vmName, 762 Location: env.location, 763 Tags: vmTags, 764 Properties: &compute.VirtualMachineProperties{ 765 HardwareProfile: &compute.HardwareProfile{ 766 VMSize: compute.VirtualMachineSizeTypes( 767 instanceSpec.InstanceType.Name, 768 ), 769 }, 770 StorageProfile: storageProfile, 771 OsProfile: osProfile, 772 NetworkProfile: &compute.NetworkProfile{ 773 &nics, 774 }, 775 AvailabilitySet: availabilitySetSubResource, 776 }, 777 DependsOn: vmDependsOn, 778 }) 779 780 // On Windows and CentOS, we must add the CustomScript VM 781 // extension to run the CustomData script. 782 switch seriesOS { 783 case os.Windows, os.CentOS: 784 properties, err := vmExtensionProperties(seriesOS) 785 if err != nil { 786 return errors.Annotate( 787 err, "creating virtual machine extension", 788 ) 789 } 790 resources = append(resources, armtemplates.Resource{ 791 APIVersion: computeAPIVersion, 792 Type: "Microsoft.Compute/virtualMachines/extensions", 793 Name: vmName + "/" + extensionName, 794 Location: env.location, 795 Tags: vmTags, 796 Properties: properties, 797 DependsOn: []string{"Microsoft.Compute/virtualMachines/" + vmName}, 798 }) 799 } 800 801 logger.Debugf("- creating virtual machine deployment") 802 template := armtemplates.Template{Resources: resources} 803 // NOTE(axw) VMs take a long time to go to "Succeeded", so we do not 804 // block waiting for them to be fully provisioned. This means we won't 805 // return an error from StartInstance if the VM fails provisioning; 806 // we will instead report the error via the instance's status. 807 deploymentsClient.ResponseInspector = asyncCreationRespondDecorator( 808 deploymentsClient.ResponseInspector, 809 ) 810 if err := createDeployment( 811 ctx, 812 deploymentsClient, 813 env.resourceGroup, 814 vmName, // deployment name 815 template, 816 ); err != nil { 817 return errors.Trace(err) 818 } 819 return nil 820 } 821 822 // maxFaultDomains returns the maximum number of fault domains for the 823 // given location/region. The numbers were taken from 824 // https://docs.microsoft.com/en-au/azure/virtual-machines/windows/manage-availability, 825 // as at 31 August 2017. 826 func maxFaultDomains(location string) int32 { 827 // From the page linked in the doc comment: 828 // "The number of fault domains for managed availability sets varies 829 // by region - either two or three per region." 830 // 831 // We record those that at the time of writing have 3. Anything 832 // else has at least 2, so we just assume 2. 833 switch location { 834 case 835 "eastus", 836 "eastus2", 837 "westus", 838 "centralus", 839 "northcentralus", 840 "southcentralus", 841 "northeurope", 842 "westeurope": 843 return 3 844 } 845 return 2 846 } 847 848 // waitCommonResourcesCreated waits for the "common" deployment to complete. 849 func (env *azureEnviron) waitCommonResourcesCreated() error { 850 env.mu.Lock() 851 defer env.mu.Unlock() 852 if env.commonResourcesCreated { 853 return nil 854 } 855 deployment, err := env.waitCommonResourcesCreatedLocked() 856 if err != nil { 857 return errors.Trace(err) 858 } 859 env.commonResourcesCreated = true 860 if deployment != nil { 861 // Check if the common deployment created 862 // a storage account. If it didn't, we can 863 // avoid a query for the storage account. 864 var hasStorageAccount bool 865 if deployment.Properties.Providers != nil { 866 for _, p := range *deployment.Properties.Providers { 867 if to.String(p.Namespace) != "Microsoft.Storage" { 868 continue 869 } 870 if p.ResourceTypes == nil { 871 continue 872 } 873 for _, rt := range *p.ResourceTypes { 874 if to.String(rt.ResourceType) != "storageAccounts" { 875 continue 876 } 877 hasStorageAccount = true 878 break 879 } 880 break 881 } 882 } 883 if !hasStorageAccount { 884 env.storageAccount = new(*storage.Account) 885 } 886 } 887 return nil 888 } 889 890 type deploymentIncompleteError struct { 891 error 892 } 893 894 func (env *azureEnviron) waitCommonResourcesCreatedLocked() (*resources.DeploymentExtended, error) { 895 deploymentsClient := resources.DeploymentsClient{env.resources} 896 897 // Release the lock while we're waiting, to avoid blocking others. 898 env.mu.Unlock() 899 defer env.mu.Lock() 900 901 // Wait for up to 5 minutes, with a 5 second polling interval, 902 // for the "common" deployment to be in one of the terminal 903 // states. The deployment typically takes only around 30 seconds, 904 // but we allow for a longer duration to be defensive. 905 var deployment *resources.DeploymentExtended 906 sdkCtx := stdcontext.Background() 907 waitDeployment := func() error { 908 result, err := deploymentsClient.Get(sdkCtx, env.resourceGroup, "common") 909 if err != nil { 910 if result.StatusCode == http.StatusNotFound { 911 // The controller model does not have a "common" 912 // deployment, as its common resources are created 913 // in the machine-0 deployment to keep bootstrap times 914 // optimal. Treat lack of a common deployment as an 915 // indication that the model is the controller model. 916 return nil 917 } 918 return errors.Annotate(err, "querying common deployment") 919 } 920 if result.Properties == nil { 921 return deploymentIncompleteError{errors.New("deployment incomplete")} 922 } 923 924 state := to.String(result.Properties.ProvisioningState) 925 if state == "Succeeded" { 926 // The deployment has succeeded, so the resources are 927 // ready for use. 928 deployment = &result 929 return nil 930 } 931 err = errors.Errorf("common resource deployment status is %q", state) 932 switch state { 933 case "Canceled", "Failed", "Deleted": 934 default: 935 err = deploymentIncompleteError{err} 936 } 937 return err 938 } 939 if err := retry.Call(retry.CallArgs{ 940 Func: waitDeployment, 941 IsFatalError: func(err error) bool { 942 _, ok := err.(deploymentIncompleteError) 943 return !ok 944 }, 945 Attempts: -1, 946 Delay: 5 * time.Second, 947 MaxDuration: 5 * time.Minute, 948 Clock: env.provider.config.RetryClock, 949 }); err != nil { 950 return nil, errors.Trace(err) 951 } 952 return deployment, nil 953 } 954 955 // createAvailabilitySet creates the availability set for a machine to use 956 // if it doesn't already exist, and returns the availability set's ID. The 957 // algorithm used for choosing the availability set is: 958 // - if the machine is a controller, use the availability set name 959 // "juju-controller"; 960 // - if the machine has units assigned, create an availability 961 // name with a name based on the value of the tags.JujuUnitsDeployed tag 962 // in vmTags, if it exists; 963 // - otherwise, do not assign the machine to an availability set 964 func availabilitySetName( 965 vmName string, 966 vmTags map[string]string, 967 controller bool, 968 ) (string, error) { 969 logger.Debugf("selecting availability set for %q", vmName) 970 if controller { 971 return controllerAvailabilitySet, nil 972 } 973 974 // We'll have to create an availability set. Use the name of one of the 975 // services assigned to the machine. 976 var availabilitySetName string 977 if unitNames, ok := vmTags[tags.JujuUnitsDeployed]; ok { 978 for _, unitName := range strings.Fields(unitNames) { 979 if !names.IsValidUnit(unitName) { 980 continue 981 } 982 serviceName, err := names.UnitApplication(unitName) 983 if err != nil { 984 return "", errors.Annotate(err, "getting application name") 985 } 986 availabilitySetName = serviceName 987 break 988 } 989 } 990 return availabilitySetName, nil 991 } 992 993 // newStorageProfile creates the storage profile for a virtual machine, 994 // based on the series and chosen instance spec. 995 func newStorageProfile( 996 vmName string, 997 maybeStorageAccount *storage.Account, 998 storageAccountType string, 999 instanceSpec *instances.InstanceSpec, 1000 ) (*compute.StorageProfile, error) { 1001 logger.Debugf("creating storage profile for %q", vmName) 1002 1003 urnParts := strings.SplitN(instanceSpec.Image.Id, ":", 4) 1004 if len(urnParts) != 4 { 1005 return nil, errors.Errorf("invalid image ID %q", instanceSpec.Image.Id) 1006 } 1007 publisher := urnParts[0] 1008 offer := urnParts[1] 1009 sku := urnParts[2] 1010 version := urnParts[3] 1011 1012 osDiskName := vmName 1013 osDiskSizeGB := mibToGB(instanceSpec.InstanceType.RootDisk) 1014 osDisk := &compute.OSDisk{ 1015 Name: to.StringPtr(osDiskName), 1016 CreateOption: compute.DiskCreateOptionTypesFromImage, 1017 Caching: compute.CachingTypesReadWrite, 1018 DiskSizeGB: to.Int32Ptr(int32(osDiskSizeGB)), 1019 } 1020 1021 if maybeStorageAccount == nil { 1022 // This model uses managed disks. 1023 osDisk.ManagedDisk = &compute.ManagedDiskParameters{ 1024 StorageAccountType: compute.StorageAccountTypes(storageAccountType), 1025 } 1026 } else { 1027 // This model uses unmanaged disks. 1028 osDiskVhdRoot := blobContainerURL(maybeStorageAccount, osDiskVHDContainer) 1029 vhdURI := osDiskVhdRoot + osDiskName + vhdExtension 1030 osDisk.Vhd = &compute.VirtualHardDisk{to.StringPtr(vhdURI)} 1031 } 1032 1033 return &compute.StorageProfile{ 1034 ImageReference: &compute.ImageReference{ 1035 Publisher: to.StringPtr(publisher), 1036 Offer: to.StringPtr(offer), 1037 Sku: to.StringPtr(sku), 1038 Version: to.StringPtr(version), 1039 }, 1040 OsDisk: osDisk, 1041 }, nil 1042 } 1043 1044 func mibToGB(mib uint64) uint64 { 1045 b := float64(mib * 1024 * 1024) 1046 return uint64(b / (1000 * 1000 * 1000)) 1047 } 1048 1049 func newOSProfile( 1050 vmName string, 1051 instanceConfig *instancecfg.InstanceConfig, 1052 randomAdminPassword func() string, 1053 generateSSHKey func(string) (string, string, error), 1054 ) (*compute.OSProfile, os.OSType, error) { 1055 logger.Debugf("creating OS profile for %q", vmName) 1056 1057 customData, err := providerinit.ComposeUserData(instanceConfig, nil, AzureRenderer{}) 1058 if err != nil { 1059 return nil, os.Unknown, errors.Annotate(err, "composing user data") 1060 } 1061 1062 osProfile := &compute.OSProfile{ 1063 ComputerName: to.StringPtr(vmName), 1064 CustomData: to.StringPtr(string(customData)), 1065 } 1066 1067 seriesOS, err := jujuseries.GetOSFromSeries(instanceConfig.Series) 1068 if err != nil { 1069 return nil, os.Unknown, errors.Trace(err) 1070 } 1071 switch seriesOS { 1072 case os.Ubuntu, os.CentOS: 1073 // SSH keys are handled by custom data, but must also be 1074 // specified in order to forego providing a password, and 1075 // disable password authentication. 1076 authorizedKeys := instanceConfig.AuthorizedKeys 1077 if len(authorizedKeys) == 0 { 1078 // Azure requires that machines be provisioned with 1079 // either a password or at least one SSH key. We 1080 // generate a key-pair to make Azure happy, but throw 1081 // away the private key so that nobody will be able 1082 // to log into the machine directly unless the keys 1083 // are updated with one that Juju tracks. 1084 _, public, err := generateSSHKey("") 1085 if err != nil { 1086 return nil, os.Unknown, errors.Trace(err) 1087 } 1088 authorizedKeys = public 1089 } 1090 1091 publicKeys := []compute.SSHPublicKey{{ 1092 Path: to.StringPtr("/home/ubuntu/.ssh/authorized_keys"), 1093 KeyData: to.StringPtr(authorizedKeys), 1094 }} 1095 osProfile.AdminUsername = to.StringPtr("ubuntu") 1096 osProfile.LinuxConfiguration = &compute.LinuxConfiguration{ 1097 DisablePasswordAuthentication: to.BoolPtr(true), 1098 SSH: &compute.SSHConfiguration{PublicKeys: &publicKeys}, 1099 } 1100 case os.Windows: 1101 osProfile.AdminUsername = to.StringPtr("JujuAdministrator") 1102 // A password is required by Azure, but we will never use it. 1103 // We generate something sufficiently long and random that it 1104 // should be infeasible to guess. 1105 osProfile.AdminPassword = to.StringPtr(randomAdminPassword()) 1106 osProfile.WindowsConfiguration = &compute.WindowsConfiguration{ 1107 ProvisionVMAgent: to.BoolPtr(true), 1108 EnableAutomaticUpdates: to.BoolPtr(true), 1109 // TODO(?) add WinRM configuration here. 1110 } 1111 default: 1112 return nil, os.Unknown, errors.NotSupportedf("%s", seriesOS) 1113 } 1114 return osProfile, seriesOS, nil 1115 } 1116 1117 // StopInstances is specified in the InstanceBroker interface. 1118 func (env *azureEnviron) StopInstances(ctx context.ProviderCallContext, ids ...instance.Id) error { 1119 if len(ids) == 0 { 1120 return nil 1121 } 1122 1123 // First up, cancel the deployments. Then we can identify the resources 1124 // that need to be deleted without racing with their creation. 1125 var wg sync.WaitGroup 1126 var existing int 1127 cancelResults := make([]error, len(ids)) 1128 for i, id := range ids { 1129 logger.Debugf("canceling deployment for instance %q", id) 1130 wg.Add(1) 1131 go func(i int, id instance.Id) { 1132 defer wg.Done() 1133 sdkCtx := stdcontext.Background() 1134 cancelResults[i] = errors.Annotatef( 1135 env.cancelDeployment(ctx, sdkCtx, string(id)), 1136 "canceling deployment %q", id, 1137 ) 1138 }(i, id) 1139 } 1140 wg.Wait() 1141 for _, err := range cancelResults { 1142 if err == nil { 1143 existing++ 1144 } else if !errors.IsNotFound(err) { 1145 return err 1146 } 1147 } 1148 if existing == 0 { 1149 // None of the instances exist, so we can stop now. 1150 return nil 1151 } 1152 1153 maybeStorageClient, _, err := env.maybeGetStorageClient() 1154 if err != nil { 1155 return errors.Trace(err) 1156 } 1157 1158 // List network interfaces and public IP addresses. 1159 instanceNics, err := instanceNetworkInterfaces( 1160 ctx, 1161 env.resourceGroup, 1162 network.InterfacesClient{env.network}, 1163 ) 1164 if err != nil { 1165 return errors.Trace(err) 1166 } 1167 instancePips, err := instancePublicIPAddresses( 1168 ctx, 1169 env.resourceGroup, 1170 network.PublicIPAddressesClient{env.network}, 1171 ) 1172 if err != nil { 1173 return errors.Trace(err) 1174 } 1175 1176 // Delete the deployments, virtual machines, and related resources. 1177 deleteResults := make([]error, len(ids)) 1178 for i, id := range ids { 1179 if errors.IsNotFound(cancelResults[i]) { 1180 continue 1181 } 1182 // The deployment does not exist, so there's nothing more to do. 1183 logger.Debugf("deleting instance %q", id) 1184 wg.Add(1) 1185 go func(i int, id instance.Id) { 1186 defer wg.Done() 1187 sdkCtx := stdcontext.Background() 1188 err := env.deleteVirtualMachine( 1189 ctx, 1190 sdkCtx, 1191 id, 1192 maybeStorageClient, 1193 instanceNics[id], 1194 instancePips[id], 1195 ) 1196 deleteResults[i] = errors.Annotatef( 1197 err, "deleting instance %q", id, 1198 ) 1199 }(i, id) 1200 } 1201 wg.Wait() 1202 for _, err := range deleteResults { 1203 if err != nil && !errors.IsNotFound(err) { 1204 return errors.Trace(err) 1205 } 1206 } 1207 1208 return nil 1209 } 1210 1211 // cancelDeployment cancels a template deployment. 1212 func (env *azureEnviron) cancelDeployment(ctx context.ProviderCallContext, sdkCtx stdcontext.Context, name string) error { 1213 deploymentsClient := resources.DeploymentsClient{env.resources} 1214 logger.Debugf("- canceling deployment %q", name) 1215 cancelResult, err := deploymentsClient.Cancel(sdkCtx, env.resourceGroup, name) 1216 if err != nil { 1217 if cancelResult.Response != nil { 1218 switch cancelResult.StatusCode { 1219 case http.StatusNotFound: 1220 return errors.NewNotFound(err, fmt.Sprintf("deployment %q not found", name)) 1221 case http.StatusConflict: 1222 if err, ok := errorutils.ServiceError(err); ok { 1223 if err.Code == serviceErrorCodeDeploymentCannotBeCancelled || 1224 err.Code == serviceErrorCodeResourceGroupBeingDeleted { 1225 // Deployments can only canceled while they're running. 1226 return nil 1227 } 1228 } 1229 } 1230 } 1231 return errorutils.HandleCredentialError(errors.Annotatef(err, "canceling deployment %q", name), ctx) 1232 } 1233 return nil 1234 } 1235 1236 // deleteVirtualMachine deletes a virtual machine and all of the resources that 1237 // it owns, and any corresponding network security rules. 1238 func (env *azureEnviron) deleteVirtualMachine( 1239 ctx context.ProviderCallContext, 1240 sdkCtx stdcontext.Context, 1241 instId instance.Id, 1242 maybeStorageClient internalazurestorage.Client, 1243 networkInterfaces []network.Interface, 1244 publicIPAddresses []network.PublicIPAddress, 1245 ) error { 1246 vmClient := compute.VirtualMachinesClient{env.compute} 1247 diskClient := compute.DisksClient{env.disk} 1248 nicClient := network.InterfacesClient{env.network} 1249 nsgClient := network.SecurityGroupsClient{env.network} 1250 securityRuleClient := network.SecurityRulesClient{env.network} 1251 pipClient := network.PublicIPAddressesClient{env.network} 1252 deploymentsClient := resources.DeploymentsClient{env.resources} 1253 vmName := string(instId) 1254 1255 // TODO(axw) delete resources concurrently. 1256 1257 // The VM must be deleted first, to release the lock on its resources. 1258 logger.Debugf("- deleting virtual machine (%s)", vmName) 1259 vmErrMsg := "deleting virtual machine" 1260 vmFuture, err := vmClient.Delete(sdkCtx, env.resourceGroup, vmName) 1261 if err != nil { 1262 if errorutils.MaybeInvalidateCredential(err, ctx) || !isNotFoundResponse(vmFuture.Response()) { 1263 return errors.Annotate(err, vmErrMsg) 1264 } 1265 } else { 1266 err = vmFuture.WaitForCompletionRef(sdkCtx, vmClient.Client) 1267 if err != nil { 1268 return errorutils.HandleCredentialError(errors.Annotate(err, vmErrMsg), ctx) 1269 } 1270 result, err := vmFuture.Result(vmClient) 1271 if err != nil { 1272 if errorutils.MaybeInvalidateCredential(err, ctx) || !isNotFoundResult(result) { 1273 return errors.Annotate(err, vmErrMsg) 1274 } 1275 } 1276 } 1277 if maybeStorageClient != nil { 1278 logger.Debugf("- deleting OS VHD (%s)", vmName) 1279 blobClient := maybeStorageClient.GetBlobService() 1280 vhdContainer := blobClient.GetContainerReference(osDiskVHDContainer) 1281 vhdBlob := vhdContainer.Blob(vmName) 1282 _, err := vhdBlob.DeleteIfExists(nil) 1283 return errorutils.HandleCredentialError(errors.Annotate(err, "deleting OS VHD"), ctx) 1284 } 1285 1286 // Delete the managed OS disk. 1287 logger.Debugf("- deleting OS disk (%s)", vmName) 1288 diskErrMsg := "deleting OS disk" 1289 diskFuture, err := diskClient.Delete(sdkCtx, env.resourceGroup, vmName) 1290 if err != nil { 1291 if errorutils.MaybeInvalidateCredential(err, ctx) || !isNotFoundResponse(diskFuture.Response()) { 1292 return errors.Annotate(err, diskErrMsg) 1293 } 1294 } 1295 if err == nil { 1296 err = diskFuture.WaitForCompletionRef(sdkCtx, diskClient.Client) 1297 if err != nil { 1298 return errorutils.HandleCredentialError(errors.Annotate(err, diskErrMsg), ctx) 1299 } 1300 result, err := diskFuture.Result(diskClient) 1301 if err != nil { 1302 if errorutils.MaybeInvalidateCredential(err, ctx) || !isNotFoundResult(result) { 1303 return errors.Annotate(err, diskErrMsg) 1304 } 1305 } 1306 } 1307 logger.Debugf("- deleting security rules (%s)", vmName) 1308 if err := deleteInstanceNetworkSecurityRules( 1309 ctx, 1310 env.resourceGroup, instId, 1311 nsgClient, securityRuleClient, 1312 ); err != nil { 1313 return errors.Annotate(err, "deleting network security rules") 1314 } 1315 1316 logger.Debugf("- deleting network interfaces (%s)", vmName) 1317 networkErrMsg := "deleting NIC" 1318 for _, nic := range networkInterfaces { 1319 nicName := to.String(nic.Name) 1320 logger.Tracef("deleting NIC %q", nicName) 1321 nicFuture, err := nicClient.Delete(sdkCtx, env.resourceGroup, nicName) 1322 if err != nil { 1323 if errorutils.MaybeInvalidateCredential(err, ctx) || !isNotFoundResponse(nicFuture.Response()) { 1324 return errors.Annotate(err, networkErrMsg) 1325 } 1326 } else { 1327 err = nicFuture.WaitForCompletionRef(sdkCtx, nicClient.Client) 1328 if err != nil { 1329 return errorutils.HandleCredentialError(errors.Annotate(err, networkErrMsg), ctx) 1330 } 1331 result, err := nicFuture.Result(nicClient) 1332 if err != nil { 1333 if errorutils.MaybeInvalidateCredential(err, ctx) || !isNotFoundResult(result) { 1334 return errors.Annotate(err, networkErrMsg) 1335 } 1336 } 1337 } 1338 } 1339 1340 logger.Debugf("- deleting public IPs (%s)", vmName) 1341 ipErrMsg := "deleting public IP" 1342 for _, pip := range publicIPAddresses { 1343 pipName := to.String(pip.Name) 1344 logger.Tracef("deleting public IP %q", pipName) 1345 ipFuture, err := pipClient.Delete(sdkCtx, env.resourceGroup, pipName) 1346 if err != nil { 1347 if errorutils.MaybeInvalidateCredential(err, ctx) || !isNotFoundResponse(ipFuture.Response()) { 1348 return errors.Annotate(err, ipErrMsg) 1349 } 1350 } else { 1351 err = ipFuture.WaitForCompletionRef(sdkCtx, pipClient.Client) 1352 if err != nil { 1353 return errorutils.HandleCredentialError(errors.Annotate(err, ipErrMsg), ctx) 1354 } 1355 result, err := ipFuture.Result(pipClient) 1356 if err != nil { 1357 if errorutils.MaybeInvalidateCredential(err, ctx) || !isNotFoundResult(result) { 1358 return errors.Annotate(err, ipErrMsg) 1359 } 1360 } 1361 } 1362 } 1363 1364 // The deployment must be deleted last, or we risk leaking resources. 1365 logger.Debugf("- deleting deployment (%s)", vmName) 1366 deploymentFuture, err := deploymentsClient.Delete(sdkCtx, env.resourceGroup, vmName) 1367 deploymentErrMsg := "deleting deployment" 1368 if err != nil { 1369 if errorutils.MaybeInvalidateCredential(err, ctx) || !isNotFoundResponse(deploymentFuture.Response()) { 1370 return errors.Annotate(err, deploymentErrMsg) 1371 } 1372 } else { 1373 err = deploymentFuture.WaitForCompletionRef(sdkCtx, deploymentsClient.Client) 1374 if err != nil { 1375 return errorutils.HandleCredentialError(errors.Annotate(err, deploymentErrMsg), ctx) 1376 } 1377 deploymentResult, err := deploymentFuture.Result(deploymentsClient) 1378 if err != nil { 1379 if errorutils.MaybeInvalidateCredential(err, ctx) || !isNotFoundResult(deploymentResult) { 1380 return errors.Annotate(err, deploymentErrMsg) 1381 } 1382 } 1383 } 1384 return nil 1385 } 1386 1387 // Instances is specified in the Environ interface. 1388 func (env *azureEnviron) Instances(ctx context.ProviderCallContext, ids []instance.Id) ([]instances.Instance, error) { 1389 return env.instances(ctx, env.resourceGroup, ids, true /* refresh addresses */) 1390 } 1391 1392 func (env *azureEnviron) instances( 1393 ctx context.ProviderCallContext, 1394 resourceGroup string, 1395 ids []instance.Id, 1396 refreshAddresses bool, 1397 ) ([]instances.Instance, error) { 1398 if len(ids) == 0 { 1399 return nil, nil 1400 } 1401 all, err := env.allInstances(ctx, resourceGroup, refreshAddresses, false) 1402 if err != nil { 1403 return nil, errors.Trace(err) 1404 } 1405 byId := make(map[instance.Id]instances.Instance) 1406 for _, inst := range all { 1407 byId[inst.Id()] = inst 1408 } 1409 var found int 1410 matching := make([]instances.Instance, len(ids)) 1411 for i, id := range ids { 1412 inst, ok := byId[id] 1413 if !ok { 1414 continue 1415 } 1416 matching[i] = inst 1417 found++ 1418 } 1419 if found == 0 { 1420 return nil, environs.ErrNoInstances 1421 } else if found < len(ids) { 1422 return matching, environs.ErrPartialInstances 1423 } 1424 return matching, nil 1425 } 1426 1427 // AdoptResources is part of the Environ interface. 1428 func (env *azureEnviron) AdoptResources(ctx context.ProviderCallContext, controllerUUID string, fromVersion version.Number) error { 1429 groupClient := resources.GroupsClient{env.resources} 1430 1431 err := env.updateGroupControllerTag(ctx, &groupClient, env.resourceGroup, controllerUUID) 1432 if err != nil { 1433 // If we can't update the group there's no point updating the 1434 // contained resources - the group will be killed if the 1435 // controller is destroyed, taking the other things with it. 1436 return errors.Trace(err) 1437 } 1438 1439 sdkCtx := stdcontext.Background() 1440 apiVersions, err := collectAPIVersions(ctx, sdkCtx, resources.ProvidersClient{env.resources}) 1441 if err != nil { 1442 return errors.Trace(err) 1443 } 1444 1445 resourceClient := resources.Client{env.resources} 1446 res, err := resourceClient.ListByResourceGroupComplete(sdkCtx, env.resourceGroup, "", "", nil) 1447 if err != nil { 1448 return errorutils.HandleCredentialError(errors.Annotate(err, "listing resources"), ctx) 1449 } 1450 var failed []string 1451 for ; res.NotDone(); err = res.NextWithContext(sdkCtx) { 1452 if err != nil { 1453 return errors.Annotate(err, "listing resources") 1454 } 1455 resource := res.Value() 1456 apiVersion := apiVersions[to.String(resource.Type)] 1457 err := env.updateResourceControllerTag( 1458 ctx, 1459 sdkCtx, 1460 internalazureresources.ResourcesClient{&resourceClient}, 1461 resource, controllerUUID, apiVersion, 1462 ) 1463 if err != nil { 1464 name := to.String(resource.Name) 1465 logger.Errorf("error updating resource tags for %q: %v", name, err) 1466 failed = append(failed, name) 1467 } 1468 } 1469 if len(failed) > 0 { 1470 return errors.Errorf("failed to update controller for some resources: %v", failed) 1471 } 1472 1473 return nil 1474 } 1475 1476 func (env *azureEnviron) updateGroupControllerTag(ctx context.ProviderCallContext, client *resources.GroupsClient, groupName, controllerUUID string) error { 1477 sdkCtx := stdcontext.Background() 1478 group, err := client.Get(sdkCtx, groupName) 1479 if err != nil { 1480 return errorutils.HandleCredentialError(errors.Trace(err), ctx) 1481 } 1482 1483 logger.Debugf( 1484 "updating resource group %s juju controller uuid to %s", 1485 to.String(group.Name), controllerUUID, 1486 ) 1487 group.Tags[tags.JujuController] = to.StringPtr(controllerUUID) 1488 1489 // The Azure API forbids specifying ProvisioningState on the update. 1490 if group.Properties != nil { 1491 (*group.Properties).ProvisioningState = nil 1492 } 1493 1494 _, err = client.CreateOrUpdate(sdkCtx, groupName, group) 1495 return errorutils.HandleCredentialError(errors.Annotatef(err, "updating controller for resource group %q", groupName), ctx) 1496 } 1497 1498 func (env *azureEnviron) updateResourceControllerTag( 1499 ctx context.ProviderCallContext, 1500 sdkCtx stdcontext.Context, 1501 client internalazureresources.ResourcesClient, 1502 stubResource resources.GenericResource, 1503 controllerUUID string, 1504 apiVersion string, 1505 ) error { 1506 stubTags := to.StringMap(stubResource.Tags) 1507 if stubTags[tags.JujuController] == controllerUUID { 1508 // No update needed. 1509 return nil 1510 } 1511 1512 // Need to get the resource individually to ensure that the 1513 // properties are populated. 1514 resource, err := client.GetByID(sdkCtx, to.String(stubResource.ID), apiVersion) 1515 if err != nil { 1516 return errorutils.HandleCredentialError(errors.Annotatef(err, "getting full resource %q", to.String(stubResource.Name)), ctx) 1517 } 1518 1519 logger.Debugf("updating %s juju controller UUID to %s", to.String(stubResource.ID), controllerUUID) 1520 resource.Tags[tags.JujuController] = to.StringPtr(controllerUUID) 1521 _, err = client.CreateOrUpdateByID( 1522 sdkCtx, 1523 to.String(stubResource.ID), 1524 resource, 1525 apiVersion, 1526 ) 1527 return errorutils.HandleCredentialError(errors.Annotatef(err, "updating controller for %q", to.String(resource.Name)), ctx) 1528 } 1529 1530 // AllInstances is specified in the InstanceBroker interface. 1531 func (env *azureEnviron) AllInstances(ctx context.ProviderCallContext) ([]instances.Instance, error) { 1532 return env.allInstances(ctx, env.resourceGroup, true /* refresh addresses */, false /* all instances */) 1533 } 1534 1535 // allInstances returns all of the instances in the given resource group, 1536 // and optionally ensures that each instance's addresses are up-to-date. 1537 func (env *azureEnviron) allInstances( 1538 ctx context.ProviderCallContext, 1539 resourceGroup string, 1540 refreshAddresses bool, 1541 controllerOnly bool, 1542 ) ([]instances.Instance, error) { 1543 deploymentsClient := resources.DeploymentsClient{env.resources} 1544 sdkCtx := stdcontext.Background() 1545 deploymentsResult, err := deploymentsClient.ListByResourceGroupComplete(sdkCtx, resourceGroup, "", nil) 1546 if err != nil { 1547 if isNotFoundResult(deploymentsResult.Response().Response) { 1548 // This will occur if the resource group does not 1549 // exist, e.g. in a fresh hosted environment. 1550 return nil, nil 1551 } 1552 return nil, errorutils.HandleCredentialError(errors.Trace(err), ctx) 1553 } 1554 if deploymentsResult.Response().IsEmpty() { 1555 return nil, nil 1556 } 1557 1558 var azureInstances []*azureInstance 1559 for ; deploymentsResult.NotDone(); err = deploymentsResult.NextWithContext(sdkCtx) { 1560 if err != nil { 1561 return nil, errors.Annotate(err, "listing resources") 1562 } 1563 deployment := deploymentsResult.Value() 1564 name := to.String(deployment.Name) 1565 if _, err := names.ParseMachineTag(name); err != nil { 1566 // Deployments we create for Juju machines are named 1567 // with the machine tag. We also create a "common" 1568 // deployment, so this will exclude that VM and any 1569 // other stray deployment resources. 1570 continue 1571 } 1572 if deployment.Properties == nil || deployment.Properties.Dependencies == nil { 1573 continue 1574 } 1575 if controllerOnly && !isControllerDeployment(deployment) { 1576 continue 1577 } 1578 provisioningState := to.String(deployment.Properties.ProvisioningState) 1579 inst := &azureInstance{name, provisioningState, env, nil, nil} 1580 azureInstances = append(azureInstances, inst) 1581 } 1582 1583 if len(azureInstances) > 0 && refreshAddresses { 1584 if err := setInstanceAddresses( 1585 ctx, 1586 resourceGroup, 1587 network.InterfacesClient{env.network}, 1588 network.PublicIPAddressesClient{env.network}, 1589 azureInstances, 1590 ); err != nil { 1591 return nil, errors.Trace(err) 1592 } 1593 } 1594 1595 instances := make([]instances.Instance, len(azureInstances)) 1596 for i, inst := range azureInstances { 1597 instances[i] = inst 1598 } 1599 return instances, nil 1600 } 1601 1602 func isControllerDeployment(deployment resources.DeploymentExtended) bool { 1603 for _, d := range *deployment.Properties.Dependencies { 1604 if d.DependsOn == nil { 1605 continue 1606 } 1607 if to.String(d.ResourceType) != "Microsoft.Compute/virtualMachines" { 1608 continue 1609 } 1610 for _, on := range *d.DependsOn { 1611 if to.String(on.ResourceType) != "Microsoft.Compute/availabilitySets" { 1612 continue 1613 } 1614 if to.String(on.ResourceName) == controllerAvailabilitySet { 1615 return true 1616 } 1617 } 1618 } 1619 return false 1620 } 1621 1622 // Destroy is specified in the Environ interface. 1623 func (env *azureEnviron) Destroy(ctx context.ProviderCallContext) error { 1624 logger.Debugf("destroying model %q", env.envName) 1625 logger.Debugf("- deleting resource group %q", env.resourceGroup) 1626 sdkCtx := stdcontext.Background() 1627 if err := env.deleteResourceGroup(ctx, sdkCtx, env.resourceGroup); err != nil { 1628 return errors.Trace(err) 1629 } 1630 // Resource groups are self-contained and fully encompass 1631 // all environ resources. Once you delete the group, there 1632 // is nothing else to do. 1633 return nil 1634 } 1635 1636 // DestroyController is specified in the Environ interface. 1637 func (env *azureEnviron) DestroyController(ctx context.ProviderCallContext, controllerUUID string) error { 1638 logger.Debugf("destroying model %q", env.envName) 1639 logger.Debugf("- deleting resource groups") 1640 if err := env.deleteControllerManagedResourceGroups(ctx, controllerUUID); err != nil { 1641 return errors.Trace(err) 1642 } 1643 // Resource groups are self-contained and fully encompass 1644 // all environ resources. Once you delete the group, there 1645 // is nothing else to do. 1646 return nil 1647 } 1648 1649 func (env *azureEnviron) deleteControllerManagedResourceGroups(ctx context.ProviderCallContext, controllerUUID string) error { 1650 filter := fmt.Sprintf( 1651 "tagname eq '%s' and tagvalue eq '%s'", 1652 tags.JujuController, controllerUUID, 1653 ) 1654 client := resources.GroupsClient{env.resources} 1655 sdkCtx := stdcontext.Background() 1656 result, err := client.List(sdkCtx, filter, nil) 1657 if err != nil { 1658 return errorutils.HandleCredentialError(errors.Annotate(err, "listing resource groups"), ctx) 1659 } 1660 if result.Values() == nil { 1661 return nil 1662 } 1663 1664 // Walk all the pages of results so we can get a total list of groups to remove. 1665 var groupNames []*string 1666 for ; result.NotDone(); err = result.NextWithContext(sdkCtx) { 1667 for _, group := range result.Values() { 1668 groupNames = append(groupNames, group.Name) 1669 } 1670 } 1671 // Deleting groups can take a long time, so make sure they are 1672 // deleted in parallel. 1673 var wg sync.WaitGroup 1674 errs := make([]error, len(groupNames)) 1675 for i, name := range groupNames { 1676 groupName := to.String(name) 1677 logger.Debugf(" - deleting resource group %q", groupName) 1678 wg.Add(1) 1679 go func(i int) { 1680 defer wg.Done() 1681 if err := env.deleteResourceGroup(ctx, sdkCtx, groupName); err != nil { 1682 errs[i] = errors.Annotatef( 1683 err, "deleting resource group %q", groupName, 1684 ) 1685 } 1686 }(i) 1687 } 1688 wg.Wait() 1689 1690 // If there is just one error, return it. If there are multiple, 1691 // then combine their messages. 1692 var nonNilErrs []error 1693 for _, err := range errs { 1694 if err != nil { 1695 nonNilErrs = append(nonNilErrs, err) 1696 } 1697 } 1698 switch len(nonNilErrs) { 1699 case 0: 1700 return nil 1701 case 1: 1702 return nonNilErrs[0] 1703 } 1704 combined := make([]string, len(nonNilErrs)) 1705 for i, err := range nonNilErrs { 1706 combined[i] = err.Error() 1707 } 1708 return errors.New(strings.Join(combined, "; ")) 1709 } 1710 1711 func (env *azureEnviron) deleteResourceGroup(ctx context.ProviderCallContext, sdkCtx stdcontext.Context, resourceGroup string) error { 1712 client := resources.GroupsClient{env.resources} 1713 future, err := client.Delete(sdkCtx, resourceGroup) 1714 if err != nil { 1715 errorutils.HandleCredentialError(err, ctx) 1716 if !isNotFoundResponse(future.Response()) { 1717 return errors.Annotatef(err, "deleting resource group %q", resourceGroup) 1718 } 1719 return nil 1720 } 1721 err = future.WaitForCompletionRef(sdkCtx, client.Client) 1722 if err != nil { 1723 return errors.Annotatef(err, "deleting resource group %q", resourceGroup) 1724 } 1725 result, err := future.Result(client) 1726 if err != nil && !isNotFoundResult(result) { 1727 return errors.Annotatef(err, "deleting resource group %q", resourceGroup) 1728 } 1729 return nil 1730 } 1731 1732 // Provider is specified in the Environ interface. 1733 func (env *azureEnviron) Provider() environs.EnvironProvider { 1734 return env.provider 1735 } 1736 1737 // resourceGroupName returns the name of the environment's resource group. 1738 func resourceGroupName(modelTag names.ModelTag, modelName string) string { 1739 return fmt.Sprintf("juju-%s-%s", modelName, resourceName(modelTag)) 1740 } 1741 1742 // resourceName returns the string to use for a resource's Name tag, 1743 // to help users identify Juju-managed resources in the Azure portal. 1744 // 1745 // Since resources are grouped under resource groups, we just use the 1746 // tag. 1747 func resourceName(tag names.Tag) string { 1748 return tag.String() 1749 } 1750 1751 // getInstanceTypes gets the instance types available for the configured 1752 // location, keyed by name. 1753 func (env *azureEnviron) getInstanceTypes(ctx context.ProviderCallContext) (map[string]instances.InstanceType, error) { 1754 env.mu.Lock() 1755 defer env.mu.Unlock() 1756 instanceTypes, err := env.getInstanceTypesLocked(ctx) 1757 if err != nil { 1758 return nil, errors.Annotate(err, "getting instance types") 1759 } 1760 return instanceTypes, nil 1761 } 1762 1763 // getInstanceTypesLocked returns the instance types for Azure, by listing the 1764 // role sizes available to the subscription. 1765 func (env *azureEnviron) getInstanceTypesLocked(ctx context.ProviderCallContext) (map[string]instances.InstanceType, error) { 1766 if env.instanceTypes != nil { 1767 return env.instanceTypes, nil 1768 } 1769 1770 location := env.location 1771 client := compute.VirtualMachineSizesClient{env.compute} 1772 1773 result, err := client.List(stdcontext.Background(), location) 1774 if err != nil { 1775 return nil, errorutils.HandleCredentialError(errors.Annotate(err, "listing VM sizes"), ctx) 1776 } 1777 instanceTypes := make(map[string]instances.InstanceType) 1778 if result.Value != nil { 1779 for _, size := range *result.Value { 1780 instanceType := newInstanceType(size) 1781 instanceTypes[instanceType.Name] = instanceType 1782 // Create aliases for standard role sizes. 1783 if strings.HasPrefix(instanceType.Name, "Standard_") { 1784 instanceTypes[instanceType.Name[len("Standard_"):]] = instanceType 1785 } 1786 } 1787 } 1788 env.instanceTypes = instanceTypes 1789 return instanceTypes, nil 1790 } 1791 1792 // maybeGetStorageClient returns the environment's storage client if it 1793 // has one, and nil if it does not. 1794 func (env *azureEnviron) maybeGetStorageClient() (internalazurestorage.Client, *storage.Account, error) { 1795 storageClient, storageAccount, err := env.getStorageClient() 1796 if errors.IsNotFound(err) { 1797 // Only models created prior to Juju 2.3 will have a storage 1798 // account. Juju 2.3 onwards exclusively uses managed disks 1799 // for all new models, and handles both managed and unmanaged 1800 // disks for upgraded models. 1801 storageClient = nil 1802 storageAccount = nil 1803 } else if err != nil { 1804 return nil, nil, errors.Trace(err) 1805 } 1806 return storageClient, storageAccount, nil 1807 } 1808 1809 // getStorageClient queries the storage account key, and uses it to construct 1810 // a new storage client. 1811 func (env *azureEnviron) getStorageClient() (internalazurestorage.Client, *storage.Account, error) { 1812 env.mu.Lock() 1813 defer env.mu.Unlock() 1814 storageAccount, err := env.getStorageAccountLocked() 1815 if err != nil { 1816 return nil, nil, errors.Annotate(err, "getting storage account") 1817 } 1818 storageAccountKey, err := env.getStorageAccountKeyLocked( 1819 to.String(storageAccount.Name), false, 1820 ) 1821 if err != nil { 1822 return nil, nil, errors.Annotate(err, "getting storage account key") 1823 } 1824 client, err := getStorageClient( 1825 env.provider.config.NewStorageClient, 1826 env.storageEndpoint, 1827 storageAccount, 1828 storageAccountKey, 1829 ) 1830 if err != nil { 1831 return nil, nil, errors.Annotate(err, "getting storage client") 1832 } 1833 return client, storageAccount, nil 1834 } 1835 1836 // getStorageAccount returns the storage account for this environment's 1837 // resource group. 1838 func (env *azureEnviron) getStorageAccount() (*storage.Account, error) { 1839 env.mu.Lock() 1840 defer env.mu.Unlock() 1841 return env.getStorageAccountLocked() 1842 } 1843 1844 func (env *azureEnviron) getStorageAccountLocked() (*storage.Account, error) { 1845 if env.storageAccount != nil { 1846 if *env.storageAccount == nil { 1847 return nil, errors.NotFoundf("storage account") 1848 } 1849 return *env.storageAccount, nil 1850 } 1851 client := storage.AccountsClient{env.storage} 1852 account, err := client.GetProperties(stdcontext.Background(), env.resourceGroup, env.storageAccountName) 1853 if err != nil { 1854 if isNotFoundResult(account.Response) { 1855 // Remember that the account was not found 1856 // by storing a pointer to a nil pointer. 1857 env.storageAccount = new(*storage.Account) 1858 return nil, errors.NewNotFound(err, fmt.Sprintf("storage account not found")) 1859 } 1860 return nil, errors.Trace(err) 1861 } 1862 env.storageAccount = new(*storage.Account) 1863 *env.storageAccount = &account 1864 return &account, nil 1865 } 1866 1867 // getStorageAccountKeysLocked returns a storage account key for this 1868 // environment's storage account. If refresh is true, any cached key 1869 // will be refreshed. This method assumes that env.mu is held. 1870 func (env *azureEnviron) getStorageAccountKeyLocked(accountName string, refresh bool) (*storage.AccountKey, error) { 1871 if !refresh && env.storageAccountKey != nil { 1872 return env.storageAccountKey, nil 1873 } 1874 client := storage.AccountsClient{env.storage} 1875 key, err := getStorageAccountKey(client, env.resourceGroup, accountName) 1876 if err != nil { 1877 return nil, errors.Trace(err) 1878 } 1879 env.storageAccountKey = key 1880 return key, nil 1881 } 1882 1883 // AgentMirror is specified in the tools.HasAgentMirror interface. 1884 // 1885 // TODO(axw) 2016-04-11 #1568715 1886 // When we have image simplestreams, we should rename this to "Region", 1887 // to implement simplestreams.HasRegion. 1888 func (env *azureEnviron) AgentMirror() (simplestreams.CloudSpec, error) { 1889 return simplestreams.CloudSpec{ 1890 Region: env.location, 1891 // The endpoints published in simplestreams 1892 // data are the storage endpoints. 1893 Endpoint: fmt.Sprintf("https://%s/", env.storageEndpoint), 1894 }, nil 1895 }