github.com/mwhudson/juju@v0.0.0-20160512215208-90ff01f3497f/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 "sort" 10 "strings" 11 "sync" 12 13 "github.com/Azure/azure-sdk-for-go/Godeps/_workspace/src/github.com/Azure/go-autorest/autorest" 14 "github.com/Azure/azure-sdk-for-go/Godeps/_workspace/src/github.com/Azure/go-autorest/autorest/to" 15 "github.com/Azure/azure-sdk-for-go/arm/compute" 16 "github.com/Azure/azure-sdk-for-go/arm/network" 17 "github.com/Azure/azure-sdk-for-go/arm/resources" 18 "github.com/Azure/azure-sdk-for-go/arm/storage" 19 azurestorage "github.com/Azure/azure-sdk-for-go/storage" 20 "github.com/juju/errors" 21 "github.com/juju/loggo" 22 "github.com/juju/names" 23 "github.com/juju/utils/arch" 24 "github.com/juju/utils/os" 25 jujuseries "github.com/juju/utils/series" 26 "github.com/juju/utils/set" 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 internalazurestorage "github.com/juju/juju/provider/azure/internal/azurestorage" 39 "github.com/juju/juju/provider/common" 40 "github.com/juju/juju/state" 41 "github.com/juju/juju/state/multiwatcher" 42 "github.com/juju/juju/tools" 43 ) 44 45 const jujuMachineNameTag = tags.JujuTagPrefix + "machine-name" 46 47 type azureEnviron struct { 48 common.SupportsUnitPlacementPolicy 49 50 // provider is the azureEnvironProvider used to open this environment. 51 provider *azureEnvironProvider 52 53 // resourceGroup is the name of the Resource Group in the Azure 54 // subscription that corresponds to the environment. 55 resourceGroup string 56 57 // envName is the name of the environment. 58 envName string 59 60 mu sync.Mutex 61 config *azureModelConfig 62 instanceTypes map[string]instances.InstanceType 63 // azure management clients 64 compute compute.ManagementClient 65 resources resources.ManagementClient 66 storage storage.ManagementClient 67 network network.ManagementClient 68 storageClient azurestorage.Client 69 } 70 71 var _ environs.Environ = (*azureEnviron)(nil) 72 var _ state.Prechecker = (*azureEnviron)(nil) 73 74 // newEnviron creates a new azureEnviron. 75 func newEnviron(provider *azureEnvironProvider, cfg *config.Config) (*azureEnviron, error) { 76 env := azureEnviron{provider: provider} 77 err := env.SetConfig(cfg) 78 if err != nil { 79 return nil, err 80 } 81 modelTag := names.NewModelTag(cfg.UUID()) 82 env.resourceGroup = resourceGroupName(modelTag, cfg.Name()) 83 env.envName = cfg.Name() 84 return &env, nil 85 } 86 87 // Bootstrap is specified in the Environ interface. 88 func (env *azureEnviron) Bootstrap( 89 ctx environs.BootstrapContext, 90 args environs.BootstrapParams, 91 ) (*environs.BootstrapResult, error) { 92 93 cfg, err := env.initResourceGroup() 94 if err != nil { 95 return nil, errors.Annotate(err, "creating controller resource group") 96 } 97 if err := env.SetConfig(cfg); err != nil { 98 return nil, errors.Annotate(err, "updating config") 99 } 100 101 result, err := common.Bootstrap(ctx, env, args) 102 if err != nil { 103 logger.Errorf("bootstrap failed, destroying model: %v", err) 104 if err := env.Destroy(); err != nil { 105 logger.Errorf("failed to destroy model: %v", err) 106 } 107 return nil, errors.Trace(err) 108 } 109 return result, nil 110 } 111 112 // initResourceGroup creates and initialises a resource group for this 113 // environment. The resource group will have a storage account and a 114 // subnet associated with it (but not necessarily contained within: 115 // see subnet creation). 116 func (env *azureEnviron) initResourceGroup() (*config.Config, error) { 117 location := env.config.location 118 tags := tags.ResourceTags( 119 names.NewModelTag(env.config.Config.UUID()), 120 names.NewModelTag(env.config.Config.ControllerUUID()), 121 env.config, 122 ) 123 resourceGroupsClient := resources.GroupsClient{env.resources} 124 125 logger.Debugf("creating resource group %q", env.resourceGroup) 126 if err := env.callAPI(func() (autorest.Response, error) { 127 group, err := resourceGroupsClient.CreateOrUpdate(env.resourceGroup, resources.ResourceGroup{ 128 Location: to.StringPtr(location), 129 Tags: toTagsPtr(tags), 130 }) 131 return group.Response, err 132 }); err != nil { 133 return nil, errors.Annotate(err, "creating resource group") 134 } 135 136 // Create an internal network for all VMs in the 137 // resource group to connect to. 138 vnetPtr, err := createInternalVirtualNetwork( 139 env.callAPI, env.network, env.resourceGroup, location, tags, 140 ) 141 if err != nil { 142 return nil, errors.Annotate(err, "creating virtual network") 143 } 144 145 _, err = createInternalSubnet( 146 env.callAPI, env.network, env.resourceGroup, vnetPtr, location, tags, 147 ) 148 if err != nil { 149 return nil, errors.Annotate(err, "creating subnet") 150 } 151 152 // Create a storage account for the resource group. 153 storageAccountsClient := storage.AccountsClient{env.storage} 154 storageAccountName, storageAccountKey, err := createStorageAccount( 155 env.callAPI, storageAccountsClient, 156 env.config.storageAccountType, 157 env.resourceGroup, location, tags, 158 env.provider.config.StorageAccountNameGenerator, 159 ) 160 if err != nil { 161 return nil, errors.Annotate(err, "creating storage account") 162 } 163 return env.config.Config.Apply(map[string]interface{}{ 164 configAttrStorageAccount: storageAccountName, 165 configAttrStorageAccountKey: storageAccountKey, 166 }) 167 } 168 169 func createStorageAccount( 170 callAPI callAPIFunc, 171 client storage.AccountsClient, 172 accountType storage.AccountType, 173 resourceGroup string, 174 location string, 175 tags map[string]string, 176 accountNameGenerator func() string, 177 ) (string, string, error) { 178 logger.Debugf("creating storage account (finding available name)") 179 const maxAttempts = 10 180 for remaining := maxAttempts; remaining > 0; remaining-- { 181 accountName := accountNameGenerator() 182 logger.Debugf("- checking storage account name %q", accountName) 183 var result storage.CheckNameAvailabilityResult 184 if err := callAPI(func() (autorest.Response, error) { 185 var err error 186 result, err = client.CheckNameAvailability( 187 storage.AccountCheckNameAvailabilityParameters{ 188 Name: to.StringPtr(accountName), 189 // Azure is a little inconsistent with when Type is 190 // required. It's required here. 191 Type: to.StringPtr("Microsoft.Storage/storageAccounts"), 192 }, 193 ) 194 return result.Response, err 195 }); err != nil { 196 return "", "", errors.Annotate(err, "checking account name availability") 197 } 198 if !to.Bool(result.NameAvailable) { 199 logger.Debugf( 200 "%q is not available (%v): %v", 201 accountName, result.Reason, result.Message, 202 ) 203 continue 204 } 205 createParams := storage.AccountCreateParameters{ 206 Location: to.StringPtr(location), 207 Tags: toTagsPtr(tags), 208 Properties: &storage.AccountPropertiesCreateParameters{ 209 AccountType: accountType, 210 }, 211 } 212 logger.Debugf("- creating %q storage account %q", accountType, accountName) 213 // TODO(axw) account creation can fail if the account name is 214 // available, but contains profanity. We should retry a set 215 // number of times even if creating fails. 216 if err := callAPI(func() (autorest.Response, error) { 217 result, err := client.Create(resourceGroup, accountName, createParams) 218 return result.Response, err 219 }); err != nil { 220 return "", "", errors.Trace(err) 221 } 222 223 logger.Debugf("- listing storage account keys") 224 var listKeysResult storage.AccountKeys 225 if err := callAPI(func() (autorest.Response, error) { 226 var err error 227 listKeysResult, err = client.ListKeys(resourceGroup, accountName) 228 return listKeysResult.Response, err 229 }); err != nil { 230 return "", "", errors.Annotate(err, "listing storage account keys") 231 } 232 return accountName, to.String(listKeysResult.Key1), nil 233 } 234 return "", "", errors.New("could not find available storage account name") 235 } 236 237 // ControllerInstances is specified in the Environ interface. 238 func (env *azureEnviron) ControllerInstances() ([]instance.Id, error) { 239 // controllers are tagged with tags.JujuIsController, so just 240 // list the instances in the controller resource group and pick 241 // those ones out. 242 instances, err := env.allInstances(env.resourceGroup, true) 243 if err != nil { 244 return nil, err 245 } 246 var ids []instance.Id 247 for _, inst := range instances { 248 azureInstance := inst.(*azureInstance) 249 if toTags(azureInstance.Tags)[tags.JujuIsController] == "true" { 250 ids = append(ids, inst.Id()) 251 } 252 } 253 if len(ids) == 0 { 254 return nil, environs.ErrNoInstances 255 } 256 return ids, nil 257 } 258 259 // Config is specified in the Environ interface. 260 func (env *azureEnviron) Config() *config.Config { 261 env.mu.Lock() 262 defer env.mu.Unlock() 263 return env.config.Config 264 } 265 266 // SetConfig is specified in the Environ interface. 267 func (env *azureEnviron) SetConfig(cfg *config.Config) error { 268 env.mu.Lock() 269 defer env.mu.Unlock() 270 271 var old *config.Config 272 if env.config != nil { 273 old = env.config.Config 274 } 275 ecfg, err := validateConfig(cfg, old) 276 if err != nil { 277 return err 278 } 279 env.config = ecfg 280 281 // Initialise clients. 282 env.compute = compute.NewWithBaseURI(ecfg.endpoint, env.config.subscriptionId) 283 env.resources = resources.NewWithBaseURI(ecfg.endpoint, env.config.subscriptionId) 284 env.storage = storage.NewWithBaseURI(ecfg.endpoint, env.config.subscriptionId) 285 env.network = network.NewWithBaseURI(ecfg.endpoint, env.config.subscriptionId) 286 clients := map[string]*autorest.Client{ 287 "azure.compute": &env.compute.Client, 288 "azure.resources": &env.resources.Client, 289 "azure.storage": &env.storage.Client, 290 "azure.network": &env.network.Client, 291 } 292 if env.provider.config.Sender != nil { 293 env.config.token.SetSender(env.provider.config.Sender) 294 } 295 for id, client := range clients { 296 client.Authorizer = env.config.token 297 logger := loggo.GetLogger(id) 298 if env.provider.config.Sender != nil { 299 client.Sender = env.provider.config.Sender 300 } 301 client.ResponseInspector = tracingRespondDecorator(logger) 302 client.RequestInspector = tracingPrepareDecorator(logger) 303 if env.provider.config.RequestInspector != nil { 304 tracer := client.RequestInspector 305 inspector := env.provider.config.RequestInspector 306 client.RequestInspector = func(p autorest.Preparer) autorest.Preparer { 307 p = tracer(p) 308 p = inspector(p) 309 return p 310 } 311 } 312 } 313 314 // Invalidate instance types when the location changes. 315 if old != nil { 316 oldLocation := old.UnknownAttrs()["location"].(string) 317 if env.config.location != oldLocation { 318 env.instanceTypes = nil 319 } 320 } 321 322 return nil 323 } 324 325 // SupportedArchitectures is specified on the EnvironCapability interface. 326 func (env *azureEnviron) SupportedArchitectures() ([]string, error) { 327 return env.supportedArchitectures(), nil 328 } 329 330 func (env *azureEnviron) supportedArchitectures() []string { 331 return []string{arch.AMD64} 332 } 333 334 // ConstraintsValidator is defined on the Environs interface. 335 func (env *azureEnviron) ConstraintsValidator() (constraints.Validator, error) { 336 instanceTypes, err := env.getInstanceTypes() 337 if err != nil { 338 return nil, err 339 } 340 instTypeNames := make([]string, 0, len(instanceTypes)) 341 for instTypeName := range instanceTypes { 342 instTypeNames = append(instTypeNames, instTypeName) 343 } 344 sort.Strings(instTypeNames) 345 346 validator := constraints.NewValidator() 347 validator.RegisterUnsupported([]string{ 348 constraints.CpuPower, 349 constraints.Tags, 350 constraints.VirtType, 351 }) 352 validator.RegisterVocabulary( 353 constraints.Arch, 354 env.supportedArchitectures(), 355 ) 356 validator.RegisterVocabulary( 357 constraints.InstanceType, 358 instTypeNames, 359 ) 360 validator.RegisterConflicts( 361 []string{constraints.InstanceType}, 362 []string{ 363 constraints.Mem, 364 constraints.CpuCores, 365 constraints.Arch, 366 constraints.RootDisk, 367 }, 368 ) 369 return validator, nil 370 } 371 372 // PrecheckInstance is defined on the state.Prechecker interface. 373 func (env *azureEnviron) PrecheckInstance(series string, cons constraints.Value, placement string) error { 374 if placement != "" { 375 return fmt.Errorf("unknown placement directive: %s", placement) 376 } 377 if !cons.HasInstanceType() { 378 return nil 379 } 380 // Constraint has an instance-type constraint so let's see if it is valid. 381 instanceTypes, err := env.getInstanceTypes() 382 if err != nil { 383 return err 384 } 385 for _, instanceType := range instanceTypes { 386 if instanceType.Name == *cons.InstanceType { 387 return nil 388 } 389 } 390 return fmt.Errorf("invalid instance type %q", *cons.InstanceType) 391 } 392 393 // MaintainInstance is specified in the InstanceBroker interface. 394 func (*azureEnviron) MaintainInstance(args environs.StartInstanceParams) error { 395 return nil 396 } 397 398 // StartInstance is specified in the InstanceBroker interface. 399 func (env *azureEnviron) StartInstance(args environs.StartInstanceParams) (*environs.StartInstanceResult, error) { 400 // Get the required configuration and config-dependent information 401 // required to create the instance. We take the lock just once, to 402 // ensure we obtain all information based on the same configuration. 403 env.mu.Lock() 404 location := env.config.location 405 envTags := tags.ResourceTags( 406 names.NewModelTag(env.config.Config.UUID()), 407 names.NewModelTag(env.config.Config.ControllerUUID()), 408 env.config, 409 ) 410 apiPort := env.config.APIPort() 411 vmClient := compute.VirtualMachinesClient{env.compute} 412 availabilitySetClient := compute.AvailabilitySetsClient{env.compute} 413 networkClient := env.network 414 vmImagesClient := compute.VirtualMachineImagesClient{env.compute} 415 vmExtensionClient := compute.VirtualMachineExtensionsClient{env.compute} 416 imageStream := env.config.ImageStream() 417 storageEndpoint := env.config.storageEndpoint 418 storageAccountName := env.config.storageAccount 419 instanceTypes, err := env.getInstanceTypesLocked() 420 if err != nil { 421 env.mu.Unlock() 422 return nil, errors.Trace(err) 423 } 424 internalNetworkSubnet, err := env.getInternalSubnetLocked() 425 if err != nil { 426 env.mu.Unlock() 427 return nil, errors.Trace(err) 428 } 429 env.mu.Unlock() 430 431 // Identify the instance type and image to provision. 432 instanceSpec, err := findInstanceSpec( 433 vmImagesClient, 434 instanceTypes, 435 &instances.InstanceConstraint{ 436 Region: location, 437 Series: args.Tools.OneSeries(), 438 Arches: args.Tools.Arches(), 439 Constraints: args.Constraints, 440 }, 441 imageStream, 442 ) 443 if err != nil { 444 return nil, err 445 } 446 447 // Pick tools by filtering the available tools down to the architecture of 448 // the image that will be provisioned. 449 selectedTools, err := args.Tools.Match(tools.Filter{ 450 Arch: instanceSpec.Image.Arch, 451 }) 452 if err != nil { 453 return nil, errors.Trace(err) 454 } 455 logger.Infof("picked tools %q", selectedTools[0].Version) 456 457 // Finalize the instance config, which we'll render to CustomData below. 458 if err := args.InstanceConfig.SetTools(selectedTools); err != nil { 459 return nil, errors.Trace(err) 460 } 461 if err := instancecfg.FinishInstanceConfig( 462 args.InstanceConfig, env.Config(), 463 ); err != nil { 464 return nil, err 465 } 466 467 machineTag := names.NewMachineTag(args.InstanceConfig.MachineId) 468 vmName := resourceName(machineTag) 469 vmTags := make(map[string]string) 470 for k, v := range args.InstanceConfig.Tags { 471 vmTags[k] = v 472 } 473 // jujuMachineNameTag identifies the VM name, in which is encoded 474 // the Juju machine name. We tag all resources related to the 475 // machine with this. 476 vmTags[jujuMachineNameTag] = vmName 477 478 // If the machine will run a controller, then we need to open the 479 // API port for it. 480 var apiPortPtr *int 481 if multiwatcher.AnyJobNeedsState(args.InstanceConfig.Jobs...) { 482 apiPortPtr = &apiPort 483 } 484 485 vm, err := createVirtualMachine( 486 env.resourceGroup, location, vmName, 487 vmTags, envTags, 488 instanceSpec, args.InstanceConfig, 489 args.DistributionGroup, 490 env.Instances, 491 apiPortPtr, internalNetworkSubnet, 492 storageEndpoint, storageAccountName, 493 networkClient, vmClient, 494 availabilitySetClient, vmExtensionClient, 495 env.callAPI, 496 ) 497 if err != nil { 498 logger.Errorf("creating instance failed, destroying: %v", err) 499 if err := env.StopInstances(instance.Id(vmName)); err != nil { 500 logger.Errorf("could not destroy failed virtual machine: %v", err) 501 } 502 return nil, errors.Annotatef(err, "creating virtual machine %q", vmName) 503 } 504 505 // Note: the instance is initialised without addresses to keep the 506 // API chatter down. We will refresh the instance if we need to know 507 // the addresses. 508 inst := &azureInstance{vm, env, nil, nil} 509 amd64 := arch.AMD64 510 hc := &instance.HardwareCharacteristics{ 511 Arch: &amd64, 512 Mem: &instanceSpec.InstanceType.Mem, 513 RootDisk: &instanceSpec.InstanceType.RootDisk, 514 CpuCores: &instanceSpec.InstanceType.CpuCores, 515 } 516 return &environs.StartInstanceResult{ 517 Instance: inst, 518 Hardware: hc, 519 }, nil 520 } 521 522 // createVirtualMachine creates a virtual machine and related resources. 523 // 524 // All resources created are tagged with the specified "vmTags", so if 525 // this function fails then all resources can be deleted by tag. 526 func createVirtualMachine( 527 resourceGroup, location, vmName string, 528 vmTags, envTags map[string]string, 529 instanceSpec *instances.InstanceSpec, 530 instanceConfig *instancecfg.InstanceConfig, 531 distributionGroupFunc func() ([]instance.Id, error), 532 instancesFunc func([]instance.Id) ([]instance.Instance, error), 533 apiPort *int, 534 internalNetworkSubnet *network.Subnet, 535 storageEndpoint, storageAccountName string, 536 networkClient network.ManagementClient, 537 vmClient compute.VirtualMachinesClient, 538 availabilitySetClient compute.AvailabilitySetsClient, 539 vmExtensionClient compute.VirtualMachineExtensionsClient, 540 callAPI callAPIFunc, 541 ) (compute.VirtualMachine, error) { 542 543 storageProfile, err := newStorageProfile( 544 vmName, instanceConfig.Series, 545 instanceSpec, storageEndpoint, storageAccountName, 546 ) 547 if err != nil { 548 return compute.VirtualMachine{}, errors.Annotate(err, "creating storage profile") 549 } 550 551 osProfile, seriesOS, err := newOSProfile(vmName, instanceConfig) 552 if err != nil { 553 return compute.VirtualMachine{}, errors.Annotate(err, "creating OS profile") 554 } 555 556 networkProfile, err := newNetworkProfile( 557 callAPI, networkClient, 558 vmName, apiPort, internalNetworkSubnet, 559 resourceGroup, location, vmTags, 560 ) 561 if err != nil { 562 return compute.VirtualMachine{}, errors.Annotate(err, "creating network profile") 563 } 564 565 availabilitySetId, err := createAvailabilitySet( 566 callAPI, availabilitySetClient, 567 vmName, resourceGroup, location, 568 vmTags, envTags, 569 distributionGroupFunc, instancesFunc, 570 ) 571 if err != nil { 572 return compute.VirtualMachine{}, errors.Annotate(err, "creating availability set") 573 } 574 575 vmArgs := compute.VirtualMachine{ 576 Location: to.StringPtr(location), 577 Tags: toTagsPtr(vmTags), 578 Properties: &compute.VirtualMachineProperties{ 579 HardwareProfile: &compute.HardwareProfile{ 580 VMSize: compute.VirtualMachineSizeTypes( 581 instanceSpec.InstanceType.Name, 582 ), 583 }, 584 StorageProfile: storageProfile, 585 OsProfile: osProfile, 586 NetworkProfile: networkProfile, 587 AvailabilitySet: &compute.SubResource{ 588 ID: to.StringPtr(availabilitySetId), 589 }, 590 }, 591 } 592 var vm compute.VirtualMachine 593 if err := callAPI(func() (autorest.Response, error) { 594 var err error 595 vm, err = vmClient.CreateOrUpdate(resourceGroup, vmName, vmArgs) 596 return vm.Response, err 597 }); err != nil { 598 return compute.VirtualMachine{}, errors.Annotate(err, "creating virtual machine") 599 } 600 601 // On Windows and CentOS, we must add the CustomScript VM 602 // extension to run the CustomData script. 603 switch seriesOS { 604 case os.Windows, os.CentOS: 605 if err := createVMExtension( 606 callAPI, vmExtensionClient, seriesOS, 607 resourceGroup, vmName, location, vmTags, 608 ); err != nil { 609 return compute.VirtualMachine{}, errors.Annotate( 610 err, "creating virtual machine extension", 611 ) 612 } 613 } 614 return vm, nil 615 } 616 617 // createAvailabilitySet creates the availability set for a machine to use 618 // if it doesn't already exist, and returns the availability set's ID. The 619 // algorithm used for choosing the availability set is: 620 // - if there is a distribution group, use the same availability set as 621 // the instances in that group. Instances in the group may be in 622 // different availability sets (when multiple services colocated on a 623 // machine), so we pick one arbitrarily 624 // - if there is no distribution group, create an availability name with 625 // a name based on the value of the tags.JujuUnitsDeployed tag in vmTags, 626 // if it exists 627 // - if there are no units assigned to the machine, then use the "juju" 628 // availability set 629 func createAvailabilitySet( 630 callAPI callAPIFunc, 631 client compute.AvailabilitySetsClient, 632 vmName, resourceGroup, location string, 633 vmTags, envTags map[string]string, 634 distributionGroupFunc func() ([]instance.Id, error), 635 instancesFunc func([]instance.Id) ([]instance.Instance, error), 636 ) (string, error) { 637 logger.Debugf("selecting availability set for %q", vmName) 638 639 // First we check if there's a distribution group, and if so, 640 // use the availability set of the first instance we find in it. 641 var instanceIds []instance.Id 642 if distributionGroupFunc != nil { 643 var err error 644 instanceIds, err = distributionGroupFunc() 645 if err != nil { 646 return "", errors.Annotate( 647 err, "querying distribution group", 648 ) 649 } 650 } 651 instances, err := instancesFunc(instanceIds) 652 switch err { 653 case nil, environs.ErrPartialInstances, environs.ErrNoInstances: 654 default: 655 return "", errors.Annotate( 656 err, "querying distribution group instances", 657 ) 658 } 659 for _, instance := range instances { 660 if instance == nil { 661 continue 662 } 663 instance := instance.(*azureInstance) 664 availabilitySetSubResource := instance.Properties.AvailabilitySet 665 if availabilitySetSubResource == nil || availabilitySetSubResource.ID == nil { 666 continue 667 } 668 logger.Debugf("- selecting availability set of %q", instance.Name) 669 return to.String(availabilitySetSubResource.ID), nil 670 } 671 672 // We'll have to create an availability set. Use the name of one of the 673 // services assigned to the machine. 674 availabilitySetName := "juju" 675 if unitNames, ok := vmTags[tags.JujuUnitsDeployed]; ok { 676 for _, unitName := range strings.Fields(unitNames) { 677 if !names.IsValidUnit(unitName) { 678 continue 679 } 680 serviceName, err := names.UnitService(unitName) 681 if err != nil { 682 return "", errors.Annotate( 683 err, "getting service name", 684 ) 685 } 686 availabilitySetName = serviceName 687 break 688 } 689 } 690 691 logger.Debugf("- creating availability set %q", availabilitySetName) 692 var availabilitySet compute.AvailabilitySet 693 if err := callAPI(func() (autorest.Response, error) { 694 var err error 695 availabilitySet, err = client.CreateOrUpdate( 696 resourceGroup, availabilitySetName, compute.AvailabilitySet{ 697 Location: to.StringPtr(location), 698 // NOTE(axw) we do *not* want to use vmTags here, 699 // because an availability set is shared by machines. 700 Tags: toTagsPtr(envTags), 701 }, 702 ) 703 return availabilitySet.Response, err 704 }); err != nil { 705 return "", errors.Annotatef( 706 err, "creating availability set %q", availabilitySetName, 707 ) 708 } 709 return to.String(availabilitySet.ID), nil 710 } 711 712 // newStorageProfile creates the storage profile for a virtual machine, 713 // based on the series and chosen instance spec. 714 func newStorageProfile( 715 vmName string, 716 series string, 717 instanceSpec *instances.InstanceSpec, 718 storageEndpoint, storageAccountName string, 719 ) (*compute.StorageProfile, error) { 720 logger.Debugf("creating storage profile for %q", vmName) 721 722 urnParts := strings.SplitN(instanceSpec.Image.Id, ":", 4) 723 if len(urnParts) != 4 { 724 return nil, errors.Errorf("invalid image ID %q", instanceSpec.Image.Id) 725 } 726 publisher := urnParts[0] 727 offer := urnParts[1] 728 sku := urnParts[2] 729 version := urnParts[3] 730 731 osDisksRoot := osDiskVhdRoot(storageEndpoint, storageAccountName) 732 osDiskName := vmName 733 osDisk := &compute.OSDisk{ 734 Name: to.StringPtr(osDiskName), 735 CreateOption: compute.FromImage, 736 Caching: compute.ReadWrite, 737 Vhd: &compute.VirtualHardDisk{ 738 URI: to.StringPtr( 739 osDisksRoot + osDiskName + vhdExtension, 740 ), 741 }, 742 } 743 return &compute.StorageProfile{ 744 ImageReference: &compute.ImageReference{ 745 Publisher: to.StringPtr(publisher), 746 Offer: to.StringPtr(offer), 747 Sku: to.StringPtr(sku), 748 Version: to.StringPtr(version), 749 }, 750 OsDisk: osDisk, 751 }, nil 752 } 753 754 func newOSProfile(vmName string, instanceConfig *instancecfg.InstanceConfig) (*compute.OSProfile, os.OSType, error) { 755 logger.Debugf("creating OS profile for %q", vmName) 756 757 customData, err := providerinit.ComposeUserData(instanceConfig, nil, AzureRenderer{}) 758 if err != nil { 759 return nil, os.Unknown, errors.Annotate(err, "composing user data") 760 } 761 762 osProfile := &compute.OSProfile{ 763 ComputerName: to.StringPtr(vmName), 764 CustomData: to.StringPtr(string(customData)), 765 } 766 767 seriesOS, err := jujuseries.GetOSFromSeries(instanceConfig.Series) 768 if err != nil { 769 return nil, os.Unknown, errors.Trace(err) 770 } 771 switch seriesOS { 772 case os.Ubuntu, os.CentOS, os.Arch: 773 // SSH keys are handled by custom data, but must also be 774 // specified in order to forego providing a password, and 775 // disable password authentication. 776 publicKeys := []compute.SSHPublicKey{{ 777 Path: to.StringPtr("/home/ubuntu/.ssh/authorized_keys"), 778 KeyData: to.StringPtr(instanceConfig.AuthorizedKeys), 779 }} 780 osProfile.AdminUsername = to.StringPtr("ubuntu") 781 osProfile.LinuxConfiguration = &compute.LinuxConfiguration{ 782 DisablePasswordAuthentication: to.BoolPtr(true), 783 SSH: &compute.SSHConfiguration{PublicKeys: &publicKeys}, 784 } 785 case os.Windows: 786 osProfile.AdminUsername = to.StringPtr("JujuAdministrator") 787 // A password is required by Azure, but we will never use it. 788 // We generate something sufficiently long and random that it 789 // should be infeasible to guess. 790 osProfile.AdminPassword = to.StringPtr(randomAdminPassword()) 791 osProfile.WindowsConfiguration = &compute.WindowsConfiguration{ 792 ProvisionVMAgent: to.BoolPtr(true), 793 EnableAutomaticUpdates: to.BoolPtr(true), 794 // TODO(?) add WinRM configuration here. 795 } 796 default: 797 return nil, os.Unknown, errors.NotSupportedf("%s", seriesOS) 798 } 799 return osProfile, seriesOS, nil 800 } 801 802 // StopInstances is specified in the InstanceBroker interface. 803 func (env *azureEnviron) StopInstances(ids ...instance.Id) error { 804 env.mu.Lock() 805 computeClient := env.compute 806 networkClient := env.network 807 env.mu.Unlock() 808 storageClient, err := env.getStorageClient() 809 if err != nil { 810 return errors.Trace(err) 811 } 812 813 // Query the instances, so we can inspect the VirtualMachines 814 // and delete related resources. 815 instances, err := env.Instances(ids) 816 switch err { 817 case environs.ErrNoInstances: 818 return nil 819 default: 820 return errors.Trace(err) 821 case nil, environs.ErrPartialInstances: 822 // handled below 823 break 824 } 825 826 for _, inst := range instances { 827 if inst == nil { 828 continue 829 } 830 if err := deleteInstance( 831 inst.(*azureInstance), 832 env.callAPI, computeClient, networkClient, storageClient, 833 ); err != nil { 834 return errors.Annotatef(err, "deleting instance %q", inst.Id()) 835 } 836 } 837 return nil 838 } 839 840 // deleteInstances deletes a virtual machine and all of the resources that 841 // it owns, and any corresponding network security rules. 842 func deleteInstance( 843 inst *azureInstance, 844 callAPI callAPIFunc, 845 computeClient compute.ManagementClient, 846 networkClient network.ManagementClient, 847 storageClient internalazurestorage.Client, 848 ) error { 849 vmName := string(inst.Id()) 850 vmClient := compute.VirtualMachinesClient{computeClient} 851 nicClient := network.InterfacesClient{networkClient} 852 nsgClient := network.SecurityGroupsClient{networkClient} 853 securityRuleClient := network.SecurityRulesClient{networkClient} 854 publicIPClient := network.PublicIPAddressesClient{networkClient} 855 logger.Debugf("deleting instance %q", vmName) 856 857 logger.Debugf("- deleting virtual machine") 858 var deleteResult autorest.Response 859 if err := callAPI(func() (autorest.Response, error) { 860 var err error 861 deleteResult, err = vmClient.Delete(inst.env.resourceGroup, vmName) 862 return deleteResult, err 863 }); err != nil { 864 if deleteResult.Response == nil || deleteResult.StatusCode != http.StatusNotFound { 865 return errors.Annotate(err, "deleting virtual machine") 866 } 867 } 868 869 // Delete the VM's OS disk VHD. 870 logger.Debugf("- deleting OS VHD") 871 blobClient := storageClient.GetBlobService() 872 if _, err := blobClient.DeleteBlobIfExists(osDiskVHDContainer, vmName); err != nil { 873 return errors.Annotate(err, "deleting OS VHD") 874 } 875 876 // Delete network security rules that refer to the VM. 877 logger.Debugf("- deleting security rules") 878 if err := deleteInstanceNetworkSecurityRules( 879 inst.env.resourceGroup, inst.Id(), nsgClient, 880 securityRuleClient, inst.env.callAPI, 881 ); err != nil { 882 return errors.Annotate(err, "deleting network security rules") 883 } 884 885 // Detach public IPs from NICs. This must be done before public 886 // IPs can be deleted. In the future, VMs may not necessarily 887 // have a public IP, so we don't use the presence of a public 888 // IP to indicate the existence of an instance. 889 logger.Debugf("- detaching public IP addresses") 890 for _, nic := range inst.networkInterfaces { 891 if nic.Properties.IPConfigurations == nil { 892 continue 893 } 894 var detached bool 895 for i, ipConfiguration := range *nic.Properties.IPConfigurations { 896 if ipConfiguration.Properties.PublicIPAddress == nil { 897 continue 898 } 899 ipConfiguration.Properties.PublicIPAddress = nil 900 (*nic.Properties.IPConfigurations)[i] = ipConfiguration 901 detached = true 902 } 903 if detached { 904 if err := callAPI(func() (autorest.Response, error) { 905 result, err := nicClient.CreateOrUpdate( 906 inst.env.resourceGroup, to.String(nic.Name), nic, 907 ) 908 return result.Response, err 909 }); err != nil { 910 return errors.Annotate(err, "detaching public IP addresses") 911 } 912 } 913 } 914 915 // Delete public IPs. 916 logger.Debugf("- deleting public IPs") 917 for _, pip := range inst.publicIPAddresses { 918 pipName := to.String(pip.Name) 919 logger.Tracef("deleting public IP %q", pipName) 920 var result autorest.Response 921 if err := callAPI(func() (autorest.Response, error) { 922 var err error 923 result, err = publicIPClient.Delete(inst.env.resourceGroup, pipName) 924 return result, err 925 }); err != nil { 926 if result.Response == nil || result.StatusCode != http.StatusNotFound { 927 return errors.Annotate(err, "deleting public IP") 928 } 929 } 930 } 931 932 // Delete NICs. 933 // 934 // NOTE(axw) this *must* be deleted last, or we risk leaking resources. 935 logger.Debugf("- deleting network interfaces") 936 for _, nic := range inst.networkInterfaces { 937 nicName := to.String(nic.Name) 938 logger.Tracef("deleting NIC %q", nicName) 939 var result autorest.Response 940 if err := callAPI(func() (autorest.Response, error) { 941 var err error 942 result, err = nicClient.Delete(inst.env.resourceGroup, nicName) 943 return result, err 944 }); err != nil { 945 if result.Response == nil || result.StatusCode != http.StatusNotFound { 946 return errors.Annotate(err, "deleting NIC") 947 } 948 } 949 } 950 951 return nil 952 } 953 954 // Instances is specified in the Environ interface. 955 func (env *azureEnviron) Instances(ids []instance.Id) ([]instance.Instance, error) { 956 return env.instances(env.resourceGroup, ids, true /* refresh addresses */) 957 } 958 959 func (env *azureEnviron) instances( 960 resourceGroup string, 961 ids []instance.Id, 962 refreshAddresses bool, 963 ) ([]instance.Instance, error) { 964 if len(ids) == 0 { 965 return nil, nil 966 } 967 all, err := env.allInstances(resourceGroup, refreshAddresses) 968 if err != nil { 969 return nil, errors.Trace(err) 970 } 971 byId := make(map[instance.Id]instance.Instance) 972 for _, inst := range all { 973 byId[inst.Id()] = inst 974 } 975 var found int 976 matching := make([]instance.Instance, len(ids)) 977 for i, id := range ids { 978 inst, ok := byId[id] 979 if !ok { 980 continue 981 } 982 matching[i] = inst 983 found++ 984 } 985 if found == 0 { 986 return nil, environs.ErrNoInstances 987 } else if found < len(ids) { 988 return matching, environs.ErrPartialInstances 989 } 990 return matching, nil 991 } 992 993 // AllInstances is specified in the InstanceBroker interface. 994 func (env *azureEnviron) AllInstances() ([]instance.Instance, error) { 995 return env.allInstances(env.resourceGroup, true /* refresh addresses */) 996 } 997 998 // allInstances returns all of the instances in the given resource group, 999 // and optionally ensures that each instance's addresses are up-to-date. 1000 func (env *azureEnviron) allInstances( 1001 resourceGroup string, 1002 refreshAddresses bool, 1003 ) ([]instance.Instance, error) { 1004 env.mu.Lock() 1005 vmClient := compute.VirtualMachinesClient{env.compute} 1006 nicClient := network.InterfacesClient{env.network} 1007 pipClient := network.PublicIPAddressesClient{env.network} 1008 env.mu.Unlock() 1009 1010 // Due to how deleting instances works, we have to get creative about 1011 // listing instances. We list NICs and return an instance for each 1012 // unique value of the jujuMachineNameTag tag. 1013 // 1014 // The machine provisioner will call AllInstances so it can delete 1015 // unknown instances. StopInstances must delete VMs before NICs and 1016 // public IPs, because a VM cannot have less than 1 NIC. Thus, we can 1017 // potentially delete a VM but then fail to delete its NIC. 1018 var nicsResult network.InterfaceListResult 1019 if err := env.callAPI(func() (autorest.Response, error) { 1020 var err error 1021 nicsResult, err = nicClient.List(resourceGroup) 1022 return nicsResult.Response, err 1023 }); err != nil { 1024 if nicsResult.Response.Response != nil && nicsResult.StatusCode == http.StatusNotFound { 1025 // This will occur if the resource group does not 1026 // exist, e.g. in a fresh hosted environment. 1027 return nil, nil 1028 } 1029 return nil, errors.Trace(err) 1030 } 1031 if nicsResult.Value == nil || len(*nicsResult.Value) == 0 { 1032 return nil, nil 1033 } 1034 1035 // Create an azureInstance for each VM. 1036 var result compute.VirtualMachineListResult 1037 if err := env.callAPI(func() (autorest.Response, error) { 1038 var err error 1039 result, err = vmClient.List(resourceGroup) 1040 return result.Response, err 1041 }); err != nil { 1042 return nil, errors.Annotate(err, "listing virtual machines") 1043 } 1044 vmNames := make(set.Strings) 1045 var azureInstances []*azureInstance 1046 if result.Value != nil { 1047 azureInstances = make([]*azureInstance, len(*result.Value)) 1048 for i, vm := range *result.Value { 1049 inst := &azureInstance{vm, env, nil, nil} 1050 azureInstances[i] = inst 1051 vmNames.Add(to.String(vm.Name)) 1052 } 1053 } 1054 1055 // Create additional azureInstances for NICs without machines. See 1056 // comments above for rationale. This needs to happen before calling 1057 // setInstanceAddresses, so we still associate the NICs/PIPs. 1058 for _, nic := range *nicsResult.Value { 1059 vmName, ok := toTags(nic.Tags)[jujuMachineNameTag] 1060 if !ok || vmNames.Contains(vmName) { 1061 continue 1062 } 1063 vm := compute.VirtualMachine{ 1064 Name: to.StringPtr(vmName), 1065 Properties: &compute.VirtualMachineProperties{ 1066 ProvisioningState: to.StringPtr("Partially Deleted"), 1067 }, 1068 } 1069 inst := &azureInstance{vm, env, nil, nil} 1070 azureInstances = append(azureInstances, inst) 1071 vmNames.Add(to.String(vm.Name)) 1072 } 1073 1074 if len(azureInstances) > 0 && refreshAddresses { 1075 if err := setInstanceAddresses( 1076 pipClient, resourceGroup, azureInstances, nicsResult, 1077 ); err != nil { 1078 return nil, errors.Trace(err) 1079 } 1080 } 1081 instances := make([]instance.Instance, len(azureInstances)) 1082 for i, inst := range azureInstances { 1083 instances[i] = inst 1084 } 1085 return instances, nil 1086 } 1087 1088 // Destroy is specified in the Environ interface. 1089 func (env *azureEnviron) Destroy() error { 1090 logger.Debugf("destroying model %q", env.envName) 1091 logger.Debugf("- deleting resource group") 1092 if err := env.deleteResourceGroup(); err != nil { 1093 return errors.Trace(err) 1094 } 1095 // Resource groups are self-contained and fully encompass 1096 // all environ resources. Once you delete the group, there 1097 // is nothing else to do. 1098 return nil 1099 } 1100 1101 func (env *azureEnviron) deleteResourceGroup() error { 1102 client := resources.GroupsClient{env.resources} 1103 var result autorest.Response 1104 if err := env.callAPI(func() (autorest.Response, error) { 1105 var err error 1106 result, err = client.Delete(env.resourceGroup) 1107 return result, err 1108 }); err != nil { 1109 if result.Response == nil || result.StatusCode != http.StatusNotFound { 1110 return errors.Annotatef(err, "deleting resource group %q", env.resourceGroup) 1111 } 1112 } 1113 return nil 1114 } 1115 1116 var errNoFwGlobal = errors.New("global firewall mode is not supported") 1117 1118 // OpenPorts is specified in the Environ interface. However, Azure does not 1119 // support the global firewall mode. 1120 func (env *azureEnviron) OpenPorts(ports []jujunetwork.PortRange) error { 1121 return errNoFwGlobal 1122 } 1123 1124 // ClosePorts is specified in the Environ interface. However, Azure does not 1125 // support the global firewall mode. 1126 func (env *azureEnviron) ClosePorts(ports []jujunetwork.PortRange) error { 1127 return errNoFwGlobal 1128 } 1129 1130 // Ports is specified in the Environ interface. 1131 func (env *azureEnviron) Ports() ([]jujunetwork.PortRange, error) { 1132 return nil, errNoFwGlobal 1133 } 1134 1135 // Provider is specified in the Environ interface. 1136 func (env *azureEnviron) Provider() environs.EnvironProvider { 1137 return env.provider 1138 } 1139 1140 // resourceGroupName returns the name of the environment's resource group. 1141 func resourceGroupName(modelTag names.ModelTag, modelName string) string { 1142 return fmt.Sprintf("juju-%s-%s", modelName, resourceName(modelTag)) 1143 } 1144 1145 // resourceName returns the string to use for a resource's Name tag, 1146 // to help users identify Juju-managed resources in the Azure portal. 1147 // 1148 // Since resources are grouped under resource groups, we just use the 1149 // tag. 1150 func resourceName(tag names.Tag) string { 1151 return tag.String() 1152 } 1153 1154 // getInstanceTypes gets the instance types available for the configured 1155 // location, keyed by name. 1156 func (env *azureEnviron) getInstanceTypes() (map[string]instances.InstanceType, error) { 1157 env.mu.Lock() 1158 defer env.mu.Unlock() 1159 instanceTypes, err := env.getInstanceTypesLocked() 1160 if err != nil { 1161 return nil, errors.Annotate(err, "getting instance types") 1162 } 1163 return instanceTypes, nil 1164 } 1165 1166 // getInstanceTypesLocked returns the instance types for Azure, by listing the 1167 // role sizes available to the subscription. 1168 func (env *azureEnviron) getInstanceTypesLocked() (map[string]instances.InstanceType, error) { 1169 if env.instanceTypes != nil { 1170 return env.instanceTypes, nil 1171 } 1172 1173 location := env.config.location 1174 client := compute.VirtualMachineSizesClient{env.compute} 1175 1176 var result compute.VirtualMachineSizeListResult 1177 if err := env.callAPI(func() (autorest.Response, error) { 1178 var err error 1179 result, err = client.List(location) 1180 return result.Response, err 1181 }); err != nil { 1182 return nil, errors.Annotate(err, "listing VM sizes") 1183 } 1184 instanceTypes := make(map[string]instances.InstanceType) 1185 if result.Value != nil { 1186 for _, size := range *result.Value { 1187 instanceType := newInstanceType(size) 1188 instanceTypes[instanceType.Name] = instanceType 1189 // Create aliases for standard role sizes. 1190 if strings.HasPrefix(instanceType.Name, "Standard_") { 1191 instanceTypes[instanceType.Name[len("Standard_"):]] = instanceType 1192 } 1193 } 1194 } 1195 env.instanceTypes = instanceTypes 1196 return instanceTypes, nil 1197 } 1198 1199 // getInternalSubnetLocked queries the internal subnet for the environment. 1200 func (env *azureEnviron) getInternalSubnetLocked() (*network.Subnet, error) { 1201 client := network.SubnetsClient{env.network} 1202 vnetName := internalNetworkName 1203 subnetName := internalSubnetName 1204 var subnet network.Subnet 1205 if err := env.callAPI(func() (autorest.Response, error) { 1206 var err error 1207 subnet, err = client.Get(env.resourceGroup, vnetName, subnetName) 1208 return subnet.Response, err 1209 }); err != nil { 1210 return nil, errors.Annotate(err, "getting internal subnet") 1211 } 1212 return &subnet, nil 1213 } 1214 1215 // getStorageClient queries the storage account key, and uses it to construct 1216 // a new storage client. 1217 func (env *azureEnviron) getStorageClient() (internalazurestorage.Client, error) { 1218 env.mu.Lock() 1219 defer env.mu.Unlock() 1220 client, err := getStorageClient(env.provider.config.NewStorageClient, env.config) 1221 if err != nil { 1222 return nil, errors.Annotate(err, "getting storage client") 1223 } 1224 return client, nil 1225 } 1226 1227 // AgentMirror is specified in the tools.HasAgentMirror interface. 1228 // 1229 // TODO(axw) 2016-04-11 #1568715 1230 // When we have image simplestreams, we should rename this to "Region", 1231 // to implement simplestreams.HasRegion. 1232 func (env *azureEnviron) AgentMirror() (simplestreams.CloudSpec, error) { 1233 env.mu.Lock() 1234 defer env.mu.Unlock() 1235 return simplestreams.CloudSpec{ 1236 Region: env.config.location, 1237 // The endpoints published in simplestreams 1238 // data are the storage endpoints. 1239 Endpoint: fmt.Sprintf("https://%s/", env.config.storageEndpoint), 1240 }, nil 1241 } 1242 1243 func (env *azureEnviron) callAPI(f func() (autorest.Response, error)) error { 1244 return backoffAPIRequestCaller{env.provider.config.RetryClock}.call(f) 1245 }