github.com/cloudbase/juju-core@v0.0.0-20140504232958-a7271ac7912f/provider/azure/environ.go (about) 1 // Copyright 2013 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 "sync" 10 "time" 11 12 "launchpad.net/gwacl" 13 14 "launchpad.net/juju-core/constraints" 15 "launchpad.net/juju-core/environs" 16 "launchpad.net/juju-core/environs/cloudinit" 17 "launchpad.net/juju-core/environs/config" 18 "launchpad.net/juju-core/environs/imagemetadata" 19 "launchpad.net/juju-core/environs/instances" 20 "launchpad.net/juju-core/environs/simplestreams" 21 "launchpad.net/juju-core/environs/storage" 22 envtools "launchpad.net/juju-core/environs/tools" 23 "launchpad.net/juju-core/instance" 24 "launchpad.net/juju-core/provider/common" 25 "launchpad.net/juju-core/state" 26 "launchpad.net/juju-core/state/api" 27 "launchpad.net/juju-core/tools" 28 "launchpad.net/juju-core/utils/parallel" 29 ) 30 31 const ( 32 // In our initial implementation, each instance gets its own hosted 33 // service, deployment and role in Azure. The role always gets this 34 // hostname (instance==service). 35 roleHostname = "default" 36 37 // deploymentSlot says in which slot to deploy instances. Azure 38 // supports 'Production' or 'Staging'. 39 // This provider always deploys to Production. Think twice about 40 // changing that: DNS names in the staging slot work differently from 41 // those in the production slot. In Staging, Azure assigns an 42 // arbitrary hostname that we can then extract from the deployment's 43 // URL. In Production, the hostname in the deployment URL does not 44 // actually seem to resolve; instead, the service name is used as the 45 // DNS name, with ".cloudapp.net" appended. 46 deploymentSlot = "Production" 47 48 // Address space of the virtual network used by the nodes in this 49 // environement, in CIDR notation. This is the network used for 50 // machine-to-machine communication. 51 networkDefinition = "10.0.0.0/8" 52 ) 53 54 type azureEnviron struct { 55 // Except where indicated otherwise, all fields in this object should 56 // only be accessed using a lock or a snapshot. 57 sync.Mutex 58 59 // name is immutable; it does not need locking. 60 name string 61 62 // ecfg is the environment's Azure-specific configuration. 63 ecfg *azureEnvironConfig 64 65 // storage is this environ's own private storage. 66 storage storage.Storage 67 68 // storageAccountKey holds an access key to this environment's 69 // private storage. This is automatically queried from Azure on 70 // startup. 71 storageAccountKey string 72 } 73 74 // azureEnviron implements Environ and HasRegion. 75 var _ environs.Environ = (*azureEnviron)(nil) 76 var _ simplestreams.HasRegion = (*azureEnviron)(nil) 77 var _ imagemetadata.SupportsCustomSources = (*azureEnviron)(nil) 78 var _ envtools.SupportsCustomSources = (*azureEnviron)(nil) 79 80 // NewEnviron creates a new azureEnviron. 81 func NewEnviron(cfg *config.Config) (*azureEnviron, error) { 82 env := azureEnviron{name: cfg.Name()} 83 err := env.SetConfig(cfg) 84 if err != nil { 85 return nil, err 86 } 87 88 // Set up storage. 89 env.storage = &azureStorage{ 90 storageContext: &environStorageContext{environ: &env}, 91 } 92 return &env, nil 93 } 94 95 // extractStorageKey returns the primary account key from a gwacl 96 // StorageAccountKeys struct, or if there is none, the secondary one. 97 func extractStorageKey(keys *gwacl.StorageAccountKeys) string { 98 if keys.Primary != "" { 99 return keys.Primary 100 } 101 return keys.Secondary 102 } 103 104 // queryStorageAccountKey retrieves the storage account's key from Azure. 105 func (env *azureEnviron) queryStorageAccountKey() (string, error) { 106 azure, err := env.getManagementAPI() 107 if err != nil { 108 return "", err 109 } 110 defer env.releaseManagementAPI(azure) 111 112 accountName := env.getSnapshot().ecfg.storageAccountName() 113 keys, err := azure.GetStorageAccountKeys(accountName) 114 if err != nil { 115 return "", fmt.Errorf("cannot obtain storage account keys: %v", err) 116 } 117 118 key := extractStorageKey(keys) 119 if key == "" { 120 return "", fmt.Errorf("no keys available for storage account") 121 } 122 123 return key, nil 124 } 125 126 // Name is specified in the Environ interface. 127 func (env *azureEnviron) Name() string { 128 return env.name 129 } 130 131 // getSnapshot produces an atomic shallow copy of the environment object. 132 // Whenever you need to access the environment object's fields without 133 // modifying them, get a snapshot and read its fields instead. You will 134 // get a consistent view of the fields without any further locking. 135 // If you do need to modify the environment's fields, do not get a snapshot 136 // but lock the object throughout the critical section. 137 func (env *azureEnviron) getSnapshot() *azureEnviron { 138 env.Lock() 139 defer env.Unlock() 140 141 // Copy the environment. (Not the pointer, the environment itself.) 142 // This is a shallow copy. 143 snap := *env 144 // Reset the snapshot's mutex, because we just copied it while we 145 // were holding it. The snapshot will have a "clean," unlocked mutex. 146 snap.Mutex = sync.Mutex{} 147 return &snap 148 } 149 150 // getAffinityGroupName returns the name of the affinity group used by all 151 // the Services in this environment. 152 func (env *azureEnviron) getAffinityGroupName() string { 153 return env.getEnvPrefix() + "ag" 154 } 155 156 func (env *azureEnviron) createAffinityGroup() error { 157 affinityGroupName := env.getAffinityGroupName() 158 azure, err := env.getManagementAPI() 159 if err != nil { 160 return err 161 } 162 defer env.releaseManagementAPI(azure) 163 snap := env.getSnapshot() 164 location := snap.ecfg.location() 165 cag := gwacl.NewCreateAffinityGroup(affinityGroupName, affinityGroupName, affinityGroupName, location) 166 return azure.CreateAffinityGroup(&gwacl.CreateAffinityGroupRequest{ 167 CreateAffinityGroup: cag}) 168 } 169 170 func (env *azureEnviron) deleteAffinityGroup() error { 171 affinityGroupName := env.getAffinityGroupName() 172 azure, err := env.getManagementAPI() 173 if err != nil { 174 return err 175 } 176 defer env.releaseManagementAPI(azure) 177 return azure.DeleteAffinityGroup(&gwacl.DeleteAffinityGroupRequest{ 178 Name: affinityGroupName}) 179 } 180 181 // getVirtualNetworkName returns the name of the virtual network used by all 182 // the VMs in this environment. 183 func (env *azureEnviron) getVirtualNetworkName() string { 184 return env.getEnvPrefix() + "vnet" 185 } 186 187 func (env *azureEnviron) createVirtualNetwork() error { 188 vnetName := env.getVirtualNetworkName() 189 affinityGroupName := env.getAffinityGroupName() 190 azure, err := env.getManagementAPI() 191 if err != nil { 192 return err 193 } 194 defer env.releaseManagementAPI(azure) 195 virtualNetwork := gwacl.VirtualNetworkSite{ 196 Name: vnetName, 197 AffinityGroup: affinityGroupName, 198 AddressSpacePrefixes: []string{ 199 networkDefinition, 200 }, 201 } 202 return azure.AddVirtualNetworkSite(&virtualNetwork) 203 } 204 205 func (env *azureEnviron) deleteVirtualNetwork() error { 206 azure, err := env.getManagementAPI() 207 if err != nil { 208 return err 209 } 210 defer env.releaseManagementAPI(azure) 211 vnetName := env.getVirtualNetworkName() 212 return azure.RemoveVirtualNetworkSite(vnetName) 213 } 214 215 // getContainerName returns the name of the private storage account container 216 // that this environment is using. 217 func (env *azureEnviron) getContainerName() string { 218 return env.getEnvPrefix() + "private" 219 } 220 221 // Bootstrap is specified in the Environ interface. 222 func (env *azureEnviron) Bootstrap(ctx environs.BootstrapContext, cons constraints.Value) (err error) { 223 // The creation of the affinity group and the virtual network is specific to the Azure provider. 224 err = env.createAffinityGroup() 225 if err != nil { 226 return err 227 } 228 // If we fail after this point, clean up the affinity group. 229 defer func() { 230 if err != nil { 231 env.deleteAffinityGroup() 232 } 233 }() 234 err = env.createVirtualNetwork() 235 if err != nil { 236 return err 237 } 238 // If we fail after this point, clean up the virtual network. 239 defer func() { 240 if err != nil { 241 env.deleteVirtualNetwork() 242 } 243 }() 244 err = common.Bootstrap(ctx, env, cons) 245 return err 246 } 247 248 // StateInfo is specified in the Environ interface. 249 func (env *azureEnviron) StateInfo() (*state.Info, *api.Info, error) { 250 return common.StateInfo(env) 251 } 252 253 // Config is specified in the Environ interface. 254 func (env *azureEnviron) Config() *config.Config { 255 snap := env.getSnapshot() 256 return snap.ecfg.Config 257 } 258 259 // SetConfig is specified in the Environ interface. 260 func (env *azureEnviron) SetConfig(cfg *config.Config) error { 261 ecfg, err := azureEnvironProvider{}.newConfig(cfg) 262 if err != nil { 263 return err 264 } 265 266 env.Lock() 267 defer env.Unlock() 268 269 if env.ecfg != nil { 270 _, err = azureEnvironProvider{}.Validate(cfg, env.ecfg.Config) 271 if err != nil { 272 return err 273 } 274 } 275 276 env.ecfg = ecfg 277 278 // Reset storage account key. Even if we had one before, it may not 279 // be appropriate for the new config. 280 env.storageAccountKey = "" 281 282 return nil 283 } 284 285 // attemptCreateService tries to create a new hosted service on Azure, with a 286 // name it chooses (based on the given prefix), but recognizes that the name 287 // may not be available. If the name is not available, it does not treat that 288 // as an error but just returns nil. 289 func attemptCreateService(azure *gwacl.ManagementAPI, prefix string, affinityGroupName string, location string) (*gwacl.CreateHostedService, error) { 290 var err error 291 name := gwacl.MakeRandomHostedServiceName(prefix) 292 err = azure.CheckHostedServiceNameAvailability(name) 293 if err != nil { 294 // The calling function should retry. 295 return nil, nil 296 } 297 req := gwacl.NewCreateHostedServiceWithLocation(name, name, location) 298 req.AffinityGroup = affinityGroupName 299 err = azure.AddHostedService(req) 300 if err != nil { 301 return nil, err 302 } 303 return req, nil 304 } 305 306 // architectures lists the CPU architectures supported by Azure. 307 var architectures = []string{"amd64", "i386"} 308 309 // newHostedService creates a hosted service. It will make up a unique name, 310 // starting with the given prefix. 311 func newHostedService(azure *gwacl.ManagementAPI, prefix string, affinityGroupName string, location string) (*gwacl.CreateHostedService, error) { 312 var err error 313 var svc *gwacl.CreateHostedService 314 for tries := 10; tries > 0 && err == nil && svc == nil; tries-- { 315 svc, err = attemptCreateService(azure, prefix, affinityGroupName, location) 316 } 317 if err != nil { 318 return nil, fmt.Errorf("could not create hosted service: %v", err) 319 } 320 if svc == nil { 321 return nil, fmt.Errorf("could not come up with a unique hosted service name - is your randomizer initialized?") 322 } 323 return svc, nil 324 } 325 326 // selectInstanceTypeAndImage returns the appropriate instance-type name and 327 // the OS image name for launching a virtual machine with the given parameters. 328 func (env *azureEnviron) selectInstanceTypeAndImage(cons constraints.Value, series, location string) (string, string, error) { 329 ecfg := env.getSnapshot().ecfg 330 sourceImageName := ecfg.forceImageName() 331 if sourceImageName != "" { 332 // Configuration forces us to use a specific image. There may 333 // not be a suitable image in the simplestreams database. 334 // This means we can't use Juju's normal selection mechanism, 335 // because it combines instance-type and image selection: if 336 // there are no images we can use, it won't offer us an 337 // instance type either. 338 // 339 // Select the instance type using simple, Azure-specific code. 340 machineType, err := selectMachineType(gwacl.RoleSizes, defaultToBaselineSpec(cons)) 341 if err != nil { 342 return "", "", err 343 } 344 return machineType.Name, sourceImageName, nil 345 } 346 347 // Choose the most suitable instance type and OS image, based on 348 // simplestreams information. 349 // 350 // This should be the normal execution path. The user is not expected 351 // to configure a source image name in normal use. 352 constraint := instances.InstanceConstraint{ 353 Region: location, 354 Series: series, 355 Arches: architectures, 356 Constraints: cons, 357 } 358 spec, err := findInstanceSpec(env, constraint) 359 if err != nil { 360 return "", "", err 361 } 362 return spec.InstanceType.Id, spec.Image.Id, nil 363 } 364 365 // StartInstance is specified in the InstanceBroker interface. 366 func (env *azureEnviron) StartInstance(cons constraints.Value, possibleTools tools.List, 367 machineConfig *cloudinit.MachineConfig) (_ instance.Instance, _ *instance.HardwareCharacteristics, err error) { 368 369 // Declaring "err" in the function signature so that we can "defer" 370 // any cleanup that needs to run during error returns. 371 372 err = environs.FinishMachineConfig(machineConfig, env.Config(), cons) 373 if err != nil { 374 return nil, nil, err 375 } 376 377 // Pick envtools. Needed for the custom data (which is what we normally 378 // call userdata). 379 machineConfig.Tools = possibleTools[0] 380 logger.Infof("picked tools %q", machineConfig.Tools) 381 382 // Compose userdata. 383 userData, err := makeCustomData(machineConfig) 384 if err != nil { 385 return nil, nil, fmt.Errorf("custom data: %v", err) 386 } 387 388 azure, err := env.getManagementAPI() 389 if err != nil { 390 return nil, nil, err 391 } 392 defer env.releaseManagementAPI(azure) 393 394 snap := env.getSnapshot() 395 location := snap.ecfg.location() 396 service, err := newHostedService(azure.ManagementAPI, env.getEnvPrefix(), env.getAffinityGroupName(), location) 397 if err != nil { 398 return nil, nil, err 399 } 400 serviceName := service.ServiceName 401 402 // If we fail after this point, clean up the hosted service. 403 defer func() { 404 if err != nil { 405 azure.DestroyHostedService( 406 &gwacl.DestroyHostedServiceRequest{ 407 ServiceName: serviceName, 408 }) 409 } 410 }() 411 412 series := possibleTools.OneSeries() 413 instanceType, sourceImageName, err := env.selectInstanceTypeAndImage(cons, series, location) 414 if err != nil { 415 return nil, nil, err 416 } 417 418 // virtualNetworkName is the virtual network to which all the 419 // deployments in this environment belong. 420 virtualNetworkName := env.getVirtualNetworkName() 421 422 // 1. Create an OS Disk. 423 vhd := env.newOSDisk(sourceImageName) 424 425 // 2. Create a Role for a Linux machine. 426 role := env.newRole(instanceType, vhd, userData, roleHostname) 427 428 // 3. Create the Deployment object. 429 deployment := env.newDeployment(role, serviceName, serviceName, virtualNetworkName) 430 431 err = azure.AddDeployment(deployment, serviceName) 432 if err != nil { 433 return nil, nil, err 434 } 435 436 var inst instance.Instance 437 438 // From here on, remember to shut down the instance before returning 439 // any error. 440 defer func() { 441 if err != nil && inst != nil { 442 err2 := env.StopInstances([]instance.Instance{inst}) 443 if err2 != nil { 444 // Failure upon failure. Log it, but return 445 // the original error. 446 logger.Errorf("error releasing failed instance: %v", err) 447 } 448 } 449 }() 450 451 // Assign the returned instance to 'inst' so that the deferred method 452 // above can perform its check. 453 inst, err = env.getInstance(serviceName) 454 if err != nil { 455 return nil, nil, err 456 } 457 // TODO(bug 1193998) - return instance hardware characteristics as well 458 return inst, &instance.HardwareCharacteristics{}, nil 459 } 460 461 // getInstance returns an up-to-date version of the instance with the given 462 // name. 463 func (env *azureEnviron) getInstance(instanceName string) (instance.Instance, error) { 464 context, err := env.getManagementAPI() 465 if err != nil { 466 return nil, err 467 } 468 defer env.releaseManagementAPI(context) 469 service, err := context.GetHostedServiceProperties(instanceName, false) 470 if err != nil { 471 return nil, fmt.Errorf("could not get instance %q: %v", instanceName, err) 472 } 473 instance := &azureInstance{service.HostedServiceDescriptor, env} 474 return instance, nil 475 } 476 477 // newOSDisk creates a gwacl.OSVirtualHardDisk object suitable for an 478 // Azure Virtual Machine. 479 func (env *azureEnviron) newOSDisk(sourceImageName string) *gwacl.OSVirtualHardDisk { 480 vhdName := gwacl.MakeRandomDiskName("juju") 481 vhdPath := fmt.Sprintf("vhds/%s", vhdName) 482 snap := env.getSnapshot() 483 storageAccount := snap.ecfg.storageAccountName() 484 mediaLink := gwacl.CreateVirtualHardDiskMediaLink(storageAccount, vhdPath) 485 // The disk label is optional and the disk name can be omitted if 486 // mediaLink is provided. 487 return gwacl.NewOSVirtualHardDisk("", "", "", mediaLink, sourceImageName, "Linux") 488 } 489 490 // getInitialEndpoints returns a slice of the endpoints every instance should have open 491 // (ssh port, etc). 492 func (env *azureEnviron) getInitialEndpoints() []gwacl.InputEndpoint { 493 cfg := env.Config() 494 return []gwacl.InputEndpoint{ 495 { 496 LocalPort: 22, 497 Name: "sshport", 498 Port: 22, 499 Protocol: "tcp", 500 }, 501 // TODO: Ought to have this only for state servers. 502 { 503 LocalPort: cfg.StatePort(), 504 Name: "stateport", 505 Port: cfg.StatePort(), 506 Protocol: "tcp", 507 }, 508 // TODO: Ought to have this only for API servers. 509 { 510 LocalPort: cfg.APIPort(), 511 Name: "apiport", 512 Port: cfg.APIPort(), 513 Protocol: "tcp", 514 }} 515 } 516 517 // newRole creates a gwacl.Role object (an Azure Virtual Machine) which uses 518 // the given Virtual Hard Drive. 519 // 520 // The VM will have: 521 // - an 'ubuntu' user defined with an unguessable (randomly generated) password 522 // - its ssh port (TCP 22) open 523 // - its state port (TCP mongoDB) port open 524 // - its API port (TCP) open 525 // 526 // roleSize is the name of one of Azure's machine types, e.g. ExtraSmall, 527 // Large, A6 etc. 528 func (env *azureEnviron) newRole(roleSize string, vhd *gwacl.OSVirtualHardDisk, userData string, roleHostname string) *gwacl.Role { 529 // Create a Linux Configuration with the username and the password 530 // empty and disable SSH with password authentication. 531 hostname := roleHostname 532 username := "ubuntu" 533 password := gwacl.MakeRandomPassword() 534 linuxConfigurationSet := gwacl.NewLinuxProvisioningConfigurationSet(hostname, username, password, userData, "true") 535 // Generate a Network Configuration with the initially required ports 536 // open. 537 networkConfigurationSet := gwacl.NewNetworkConfigurationSet(env.getInitialEndpoints(), nil) 538 roleName := gwacl.MakeRandomRoleName("juju") 539 // The ordering of these configuration sets is significant for the tests. 540 return gwacl.NewRole( 541 roleSize, roleName, 542 []gwacl.ConfigurationSet{*linuxConfigurationSet, *networkConfigurationSet}, 543 []gwacl.OSVirtualHardDisk{*vhd}) 544 } 545 546 // newDeployment creates and returns a gwacl Deployment object. 547 func (env *azureEnviron) newDeployment(role *gwacl.Role, deploymentName string, deploymentLabel string, virtualNetworkName string) *gwacl.Deployment { 548 // Use the service name as the label for the deployment. 549 return gwacl.NewDeploymentForCreateVMDeployment(deploymentName, deploymentSlot, deploymentLabel, []gwacl.Role{*role}, virtualNetworkName) 550 } 551 552 // Spawn this many goroutines to issue requests for destroying services. 553 // TODO: this is currently set to 1 because of a problem in Azure: 554 // removing Services in the same affinity group concurrently causes a conflict. 555 // This conflict is wrongly reported by Azure as a BadRequest (400). 556 // This has been reported to Windows Azure. 557 var maxConcurrentDeletes = 1 558 559 // StartInstance is specified in the InstanceBroker interface. 560 func (env *azureEnviron) StopInstances(instances []instance.Instance) error { 561 // Each Juju instance is an Azure Service (instance==service), destroy 562 // all the Azure services. 563 // Acquire management API object. 564 context, err := env.getManagementAPI() 565 if err != nil { 566 return err 567 } 568 defer env.releaseManagementAPI(context) 569 570 // Destroy all the services in parallel. 571 run := parallel.NewRun(maxConcurrentDeletes) 572 for _, instance := range instances { 573 serviceName := string(instance.Id()) 574 run.Do(func() error { 575 request := &gwacl.DestroyHostedServiceRequest{ServiceName: serviceName} 576 return context.DestroyHostedService(request) 577 }) 578 } 579 return run.Wait() 580 } 581 582 // Instances is specified in the Environ interface. 583 func (env *azureEnviron) Instances(ids []instance.Id) ([]instance.Instance, error) { 584 // The instance list is built using the list of all the relevant 585 // Azure Services (instance==service). 586 // Acquire management API object. 587 context, err := env.getManagementAPI() 588 if err != nil { 589 return nil, err 590 } 591 defer env.releaseManagementAPI(context) 592 593 // Prepare gwacl request object. 594 serviceNames := make([]string, len(ids)) 595 for i, id := range ids { 596 serviceNames[i] = string(id) 597 } 598 request := &gwacl.ListSpecificHostedServicesRequest{ServiceNames: serviceNames} 599 600 // Issue 'ListSpecificHostedServices' request with gwacl. 601 services, err := context.ListSpecificHostedServices(request) 602 if err != nil { 603 return nil, err 604 } 605 606 // If no instances were found, return ErrNoInstances. 607 if len(services) == 0 { 608 return nil, environs.ErrNoInstances 609 } 610 611 instances := convertToInstances(services, env) 612 613 // Check if we got a partial result. 614 if len(ids) != len(instances) { 615 return instances, environs.ErrPartialInstances 616 } 617 return instances, nil 618 } 619 620 // AllInstances is specified in the InstanceBroker interface. 621 func (env *azureEnviron) AllInstances() ([]instance.Instance, error) { 622 // The instance list is built using the list of all the Azure 623 // Services (instance==service). 624 // Acquire management API object. 625 context, err := env.getManagementAPI() 626 if err != nil { 627 return nil, err 628 } 629 defer env.releaseManagementAPI(context) 630 631 request := &gwacl.ListPrefixedHostedServicesRequest{ServiceNamePrefix: env.getEnvPrefix()} 632 services, err := context.ListPrefixedHostedServices(request) 633 if err != nil { 634 return nil, err 635 } 636 return convertToInstances(services, env), nil 637 } 638 639 // getEnvPrefix returns the prefix used to name the objects specific to this 640 // environment. 641 func (env *azureEnviron) getEnvPrefix() string { 642 return fmt.Sprintf("juju-%s-", env.Name()) 643 } 644 645 // convertToInstances converts a slice of gwacl.HostedServiceDescriptor objects 646 // into a slice of instance.Instance objects. 647 func convertToInstances(services []gwacl.HostedServiceDescriptor, env *azureEnviron) []instance.Instance { 648 instances := make([]instance.Instance, len(services)) 649 for i, service := range services { 650 instances[i] = &azureInstance{service, env} 651 } 652 return instances 653 } 654 655 // Storage is specified in the Environ interface. 656 func (env *azureEnviron) Storage() storage.Storage { 657 return env.getSnapshot().storage 658 } 659 660 // Destroy is specified in the Environ interface. 661 func (env *azureEnviron) Destroy() error { 662 logger.Debugf("destroying environment %q", env.name) 663 664 // Stop all instances. 665 insts, err := env.AllInstances() 666 if err != nil { 667 return fmt.Errorf("cannot get instances: %v", err) 668 } 669 err = env.StopInstances(insts) 670 if err != nil { 671 return fmt.Errorf("cannot stop instances: %v", err) 672 } 673 674 // Delete vnet and affinity group. 675 err = env.deleteVirtualNetwork() 676 if err != nil { 677 return fmt.Errorf("cannot delete the environment's virtual network: %v", err) 678 } 679 err = env.deleteAffinityGroup() 680 if err != nil { 681 return fmt.Errorf("cannot delete the environment's affinity group: %v", err) 682 } 683 684 // Delete storage. 685 // Deleting the storage is done last so that if something fails 686 // half way through the Destroy() method, the storage won't be cleaned 687 // up and thus an attempt to re-boostrap the environment will lead to 688 // a "error: environment is already bootstrapped" error. 689 err = env.Storage().RemoveAll() 690 if err != nil { 691 return fmt.Errorf("cannot clean up storage: %v", err) 692 } 693 return nil 694 } 695 696 // OpenPorts is specified in the Environ interface. However, Azure does not 697 // support the global firewall mode. 698 func (env *azureEnviron) OpenPorts(ports []instance.Port) error { 699 return nil 700 } 701 702 // ClosePorts is specified in the Environ interface. However, Azure does not 703 // support the global firewall mode. 704 func (env *azureEnviron) ClosePorts(ports []instance.Port) error { 705 return nil 706 } 707 708 // Ports is specified in the Environ interface. 709 func (env *azureEnviron) Ports() ([]instance.Port, error) { 710 // TODO: implement this. 711 return []instance.Port{}, nil 712 } 713 714 // Provider is specified in the Environ interface. 715 func (env *azureEnviron) Provider() environs.EnvironProvider { 716 return azureEnvironProvider{} 717 } 718 719 // azureManagementContext wraps two things: a gwacl.ManagementAPI (effectively 720 // a session on the Azure management API) and a tempCertFile, which keeps track 721 // of the temporary certificate file that needs to be deleted once we're done 722 // with this particular session. 723 // Since it embeds *gwacl.ManagementAPI, you can use it much as if it were a 724 // pointer to a ManagementAPI object. Just don't forget to release it after 725 // use. 726 type azureManagementContext struct { 727 *gwacl.ManagementAPI 728 certFile *tempCertFile 729 } 730 731 var ( 732 retryPolicy = gwacl.RetryPolicy{ 733 NbRetries: 6, 734 HttpStatusCodes: []int{ 735 http.StatusConflict, 736 http.StatusRequestTimeout, 737 http.StatusInternalServerError, 738 http.StatusServiceUnavailable, 739 }, 740 Delay: 10 * time.Second} 741 ) 742 743 // getManagementAPI obtains a context object for interfacing with Azure's 744 // management API. 745 // For now, each invocation just returns a separate object. This is probably 746 // wasteful (each context gets its own SSL connection) and may need optimizing 747 // later. 748 func (env *azureEnviron) getManagementAPI() (*azureManagementContext, error) { 749 snap := env.getSnapshot() 750 subscription := snap.ecfg.managementSubscriptionId() 751 certData := snap.ecfg.managementCertificate() 752 certFile, err := newTempCertFile([]byte(certData)) 753 if err != nil { 754 return nil, err 755 } 756 // After this point, if we need to leave prematurely, we should clean 757 // up that certificate file. 758 location := snap.ecfg.location() 759 mgtAPI, err := gwacl.NewManagementAPIWithRetryPolicy(subscription, certFile.Path(), location, retryPolicy) 760 if err != nil { 761 certFile.Delete() 762 return nil, err 763 } 764 context := azureManagementContext{ 765 ManagementAPI: mgtAPI, 766 certFile: certFile, 767 } 768 return &context, nil 769 } 770 771 // releaseManagementAPI frees up a context object obtained through 772 // getManagementAPI. 773 func (env *azureEnviron) releaseManagementAPI(context *azureManagementContext) { 774 // Be tolerant to incomplete context objects, in case we ever get 775 // called during cleanup of a failed attempt to create one. 776 if context == nil || context.certFile == nil { 777 return 778 } 779 // For now, all that needs doing is to delete the temporary certificate 780 // file. We may do cleverer things later, such as connection pooling 781 // where this method returns a context to the pool. 782 context.certFile.Delete() 783 } 784 785 // updateStorageAccountKey queries the storage account key, and updates the 786 // version cached in env.storageAccountKey. 787 // 788 // It takes a snapshot in order to preserve transactional integrity relative 789 // to the snapshot's starting state, without having to lock the environment 790 // for the duration. If there is a conflicting change to env relative to the 791 // state recorded in the snapshot, this function will fail. 792 func (env *azureEnviron) updateStorageAccountKey(snapshot *azureEnviron) (string, error) { 793 // This method follows an RCU pattern, an optimistic technique to 794 // implement atomic read-update transactions: get a consistent snapshot 795 // of state; process data; enter critical section; check for conflicts; 796 // write back changes. The advantage is that there are no long-held 797 // locks, in particular while waiting for the request to Azure to 798 // complete. 799 // "Get a consistent snapshot of state" is the caller's responsibility. 800 // The caller can use env.getSnapshot(). 801 802 // Process data: get a current account key from Azure. 803 key, err := env.queryStorageAccountKey() 804 if err != nil { 805 return "", err 806 } 807 808 // Enter critical section. 809 env.Lock() 810 defer env.Unlock() 811 812 // Check for conflicts: is the config still what it was? 813 if env.ecfg != snapshot.ecfg { 814 // The environment has been reconfigured while we were 815 // working on this, so the key we just get may not be 816 // appropriate any longer. So fail. 817 // Whatever we were doing isn't likely to be right any more 818 // anyway. Otherwise, it might be worth returning the key 819 // just in case it still works, and proceed without updating 820 // env.storageAccountKey. 821 return "", fmt.Errorf("environment was reconfigured") 822 } 823 824 // Write back changes. 825 env.storageAccountKey = key 826 return key, nil 827 } 828 829 // getStorageContext obtains a context object for interfacing with Azure's 830 // storage API. 831 // For now, each invocation just returns a separate object. This is probably 832 // wasteful (each context gets its own SSL connection) and may need optimizing 833 // later. 834 func (env *azureEnviron) getStorageContext() (*gwacl.StorageContext, error) { 835 snap := env.getSnapshot() 836 key := snap.storageAccountKey 837 if key == "" { 838 // We don't know the storage-account key yet. Request it. 839 var err error 840 key, err = env.updateStorageAccountKey(snap) 841 if err != nil { 842 return nil, err 843 } 844 } 845 context := gwacl.StorageContext{ 846 Account: snap.ecfg.storageAccountName(), 847 Key: key, 848 AzureEndpoint: gwacl.GetEndpoint(snap.ecfg.location()), 849 RetryPolicy: retryPolicy, 850 } 851 return &context, nil 852 } 853 854 // baseURLs specifies an Azure specific location where we look for simplestreams information. 855 // It contains the central databases for the released and daily streams, but this may 856 // become more configurable. This variable is here as a placeholder, but also 857 // as an injection point for tests. 858 var baseURLs = []string{} 859 860 // GetImageSources returns a list of sources which are used to search for simplestreams image metadata. 861 func (env *azureEnviron) GetImageSources() ([]simplestreams.DataSource, error) { 862 sources := make([]simplestreams.DataSource, 1+len(baseURLs)) 863 sources[0] = storage.NewStorageSimpleStreamsDataSource("cloud storage", env.Storage(), storage.BaseImagesPath) 864 for i, url := range baseURLs { 865 sources[i+1] = simplestreams.NewURLDataSource("Azure base URL", url, simplestreams.VerifySSLHostnames) 866 } 867 return sources, nil 868 } 869 870 // GetToolsSources returns a list of sources which are used to search for simplestreams tools metadata. 871 func (env *azureEnviron) GetToolsSources() ([]simplestreams.DataSource, error) { 872 // Add the simplestreams source off the control bucket. 873 sources := []simplestreams.DataSource{ 874 storage.NewStorageSimpleStreamsDataSource("cloud storage", env.Storage(), storage.BaseToolsPath)} 875 return sources, nil 876 } 877 878 // getImageMetadataSigningRequired returns whether this environment requires 879 // image metadata from Simplestreams to be signed. 880 func (env *azureEnviron) getImageMetadataSigningRequired() bool { 881 // Hard-coded to true for now. Once we support custom base URLs, 882 // this may have to change. 883 return true 884 } 885 886 // Region is specified in the HasRegion interface. 887 func (env *azureEnviron) Region() (simplestreams.CloudSpec, error) { 888 ecfg := env.getSnapshot().ecfg 889 return simplestreams.CloudSpec{ 890 Region: ecfg.location(), 891 Endpoint: string(gwacl.GetEndpoint(ecfg.location())), 892 }, nil 893 }