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