github.com/mattyw/juju@v0.0.0-20140610034352-732aecd63861/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 "encoding/base64" 8 "fmt" 9 "net/http" 10 "regexp" 11 "strings" 12 "sync" 13 "time" 14 15 "github.com/juju/errors" 16 "github.com/juju/utils" 17 "github.com/juju/utils/set" 18 "launchpad.net/gwacl" 19 20 "github.com/juju/juju/constraints" 21 "github.com/juju/juju/environs" 22 "github.com/juju/juju/environs/config" 23 "github.com/juju/juju/environs/imagemetadata" 24 "github.com/juju/juju/environs/instances" 25 "github.com/juju/juju/environs/network" 26 "github.com/juju/juju/environs/simplestreams" 27 "github.com/juju/juju/environs/storage" 28 envtools "github.com/juju/juju/environs/tools" 29 "github.com/juju/juju/instance" 30 "github.com/juju/juju/provider/common" 31 "github.com/juju/juju/state" 32 "github.com/juju/juju/state/api" 33 "github.com/juju/juju/state/api/params" 34 ) 35 36 const ( 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 // stateServerLabel is the label applied to the cloud service created 54 // for state servers. 55 stateServerLabel = "juju-state-server" 56 ) 57 58 // vars for testing purposes. 59 var ( 60 createInstance = (*azureEnviron).createInstance 61 ) 62 63 type azureEnviron struct { 64 // Except where indicated otherwise, all fields in this object should 65 // only be accessed using a lock or a snapshot. 66 sync.Mutex 67 68 // name is immutable; it does not need locking. 69 name string 70 71 // archMutex gates access to supportedArchitectures 72 archMutex sync.Mutex 73 // supportedArchitectures caches the architectures 74 // for which images can be instantiated. 75 supportedArchitectures []string 76 77 // ecfg is the environment's Azure-specific configuration. 78 ecfg *azureEnvironConfig 79 80 // storage is this environ's own private storage. 81 storage storage.Storage 82 83 // storageAccountKey holds an access key to this environment's 84 // private storage. This is automatically queried from Azure on 85 // startup. 86 storageAccountKey string 87 } 88 89 // azureEnviron implements Environ and HasRegion. 90 var _ environs.Environ = (*azureEnviron)(nil) 91 var _ simplestreams.HasRegion = (*azureEnviron)(nil) 92 var _ imagemetadata.SupportsCustomSources = (*azureEnviron)(nil) 93 var _ envtools.SupportsCustomSources = (*azureEnviron)(nil) 94 var _ state.Prechecker = (*azureEnviron)(nil) 95 96 // NewEnviron creates a new azureEnviron. 97 func NewEnviron(cfg *config.Config) (*azureEnviron, error) { 98 env := azureEnviron{name: cfg.Name()} 99 err := env.SetConfig(cfg) 100 if err != nil { 101 return nil, err 102 } 103 104 // Set up storage. 105 env.storage = &azureStorage{ 106 storageContext: &environStorageContext{environ: &env}, 107 } 108 return &env, nil 109 } 110 111 // extractStorageKey returns the primary account key from a gwacl 112 // StorageAccountKeys struct, or if there is none, the secondary one. 113 func extractStorageKey(keys *gwacl.StorageAccountKeys) string { 114 if keys.Primary != "" { 115 return keys.Primary 116 } 117 return keys.Secondary 118 } 119 120 // queryStorageAccountKey retrieves the storage account's key from Azure. 121 func (env *azureEnviron) queryStorageAccountKey() (string, error) { 122 azure, err := env.getManagementAPI() 123 if err != nil { 124 return "", err 125 } 126 defer env.releaseManagementAPI(azure) 127 128 accountName := env.getSnapshot().ecfg.storageAccountName() 129 keys, err := azure.GetStorageAccountKeys(accountName) 130 if err != nil { 131 return "", fmt.Errorf("cannot obtain storage account keys: %v", err) 132 } 133 134 key := extractStorageKey(keys) 135 if key == "" { 136 return "", fmt.Errorf("no keys available for storage account") 137 } 138 139 return key, nil 140 } 141 142 // Name is specified in the Environ interface. 143 func (env *azureEnviron) Name() string { 144 return env.name 145 } 146 147 // getSnapshot produces an atomic shallow copy of the environment object. 148 // Whenever you need to access the environment object's fields without 149 // modifying them, get a snapshot and read its fields instead. You will 150 // get a consistent view of the fields without any further locking. 151 // If you do need to modify the environment's fields, do not get a snapshot 152 // but lock the object throughout the critical section. 153 func (env *azureEnviron) getSnapshot() *azureEnviron { 154 env.Lock() 155 defer env.Unlock() 156 157 // Copy the environment. (Not the pointer, the environment itself.) 158 // This is a shallow copy. 159 snap := *env 160 // Reset the snapshot's mutex, because we just copied it while we 161 // were holding it. The snapshot will have a "clean," unlocked mutex. 162 snap.Mutex = sync.Mutex{} 163 return &snap 164 } 165 166 // getAffinityGroupName returns the name of the affinity group used by all 167 // the Services in this environment. 168 func (env *azureEnviron) getAffinityGroupName() string { 169 return env.getEnvPrefix() + "ag" 170 } 171 172 func (env *azureEnviron) createAffinityGroup() error { 173 affinityGroupName := env.getAffinityGroupName() 174 azure, err := env.getManagementAPI() 175 if err != nil { 176 return err 177 } 178 defer env.releaseManagementAPI(azure) 179 snap := env.getSnapshot() 180 location := snap.ecfg.location() 181 cag := gwacl.NewCreateAffinityGroup(affinityGroupName, affinityGroupName, affinityGroupName, location) 182 return azure.CreateAffinityGroup(&gwacl.CreateAffinityGroupRequest{ 183 CreateAffinityGroup: cag}) 184 } 185 186 func (env *azureEnviron) deleteAffinityGroup() error { 187 affinityGroupName := env.getAffinityGroupName() 188 azure, err := env.getManagementAPI() 189 if err != nil { 190 return err 191 } 192 defer env.releaseManagementAPI(azure) 193 return azure.DeleteAffinityGroup(&gwacl.DeleteAffinityGroupRequest{ 194 Name: affinityGroupName}) 195 } 196 197 // getVirtualNetworkName returns the name of the virtual network used by all 198 // the VMs in this environment. 199 func (env *azureEnviron) getVirtualNetworkName() string { 200 return env.getEnvPrefix() + "vnet" 201 } 202 203 func (env *azureEnviron) createVirtualNetwork() error { 204 vnetName := env.getVirtualNetworkName() 205 affinityGroupName := env.getAffinityGroupName() 206 azure, err := env.getManagementAPI() 207 if err != nil { 208 return err 209 } 210 defer env.releaseManagementAPI(azure) 211 virtualNetwork := gwacl.VirtualNetworkSite{ 212 Name: vnetName, 213 AffinityGroup: affinityGroupName, 214 AddressSpacePrefixes: []string{ 215 networkDefinition, 216 }, 217 } 218 return azure.AddVirtualNetworkSite(&virtualNetwork) 219 } 220 221 // deleteVnetAttempt is an AttemptyStrategy for use 222 // when attempting delete a virtual network. This is 223 // necessary as Azure apparently does not release all 224 // references to the vnet even when all cloud services 225 // are deleted. 226 var deleteVnetAttempt = utils.AttemptStrategy{ 227 Total: 30 * time.Second, 228 Delay: 1 * time.Second, 229 } 230 231 var networkInUse = regexp.MustCompile(".*The virtual network .* is currently in use.*") 232 233 func (env *azureEnviron) deleteVirtualNetwork() error { 234 azure, err := env.getManagementAPI() 235 if err != nil { 236 return err 237 } 238 defer env.releaseManagementAPI(azure) 239 for a := deleteVnetAttempt.Start(); a.Next(); { 240 vnetName := env.getVirtualNetworkName() 241 err = azure.RemoveVirtualNetworkSite(vnetName) 242 if err == nil { 243 return nil 244 } 245 if err, ok := err.(*gwacl.AzureError); ok { 246 if err.StatusCode() == 400 && networkInUse.MatchString(err.Message) { 247 // Retry on "virtual network XYZ is currently in use". 248 continue 249 } 250 } 251 // Any other error should be returned. 252 break 253 } 254 return err 255 } 256 257 // getContainerName returns the name of the private storage account container 258 // that this environment is using. 259 func (env *azureEnviron) getContainerName() string { 260 return env.getEnvPrefix() + "private" 261 } 262 263 // Bootstrap is specified in the Environ interface. 264 func (env *azureEnviron) Bootstrap(ctx environs.BootstrapContext, args environs.BootstrapParams) (err error) { 265 // The creation of the affinity group and the virtual network is specific to the Azure provider. 266 err = env.createAffinityGroup() 267 if err != nil { 268 return err 269 } 270 // If we fail after this point, clean up the affinity group. 271 defer func() { 272 if err != nil { 273 env.deleteAffinityGroup() 274 } 275 }() 276 err = env.createVirtualNetwork() 277 if err != nil { 278 return err 279 } 280 // If we fail after this point, clean up the virtual network. 281 defer func() { 282 if err != nil { 283 env.deleteVirtualNetwork() 284 } 285 }() 286 err = common.Bootstrap(ctx, env, args) 287 return err 288 } 289 290 // StateInfo is specified in the Environ interface. 291 func (env *azureEnviron) StateInfo() (*state.Info, *api.Info, error) { 292 return common.StateInfo(env) 293 } 294 295 // Config is specified in the Environ interface. 296 func (env *azureEnviron) Config() *config.Config { 297 snap := env.getSnapshot() 298 return snap.ecfg.Config 299 } 300 301 // SetConfig is specified in the Environ interface. 302 func (env *azureEnviron) SetConfig(cfg *config.Config) error { 303 ecfg, err := azureEnvironProvider{}.newConfig(cfg) 304 if err != nil { 305 return err 306 } 307 308 env.Lock() 309 defer env.Unlock() 310 311 if env.ecfg != nil { 312 _, err = azureEnvironProvider{}.Validate(cfg, env.ecfg.Config) 313 if err != nil { 314 return err 315 } 316 } 317 318 env.ecfg = ecfg 319 320 // Reset storage account key. Even if we had one before, it may not 321 // be appropriate for the new config. 322 env.storageAccountKey = "" 323 324 return nil 325 } 326 327 // attemptCreateService tries to create a new hosted service on Azure, with a 328 // name it chooses (based on the given prefix), but recognizes that the name 329 // may not be available. If the name is not available, it does not treat that 330 // as an error but just returns nil. 331 func attemptCreateService(azure *gwacl.ManagementAPI, prefix, affinityGroupName, label string) (*gwacl.CreateHostedService, error) { 332 var err error 333 name := gwacl.MakeRandomHostedServiceName(prefix) 334 err = azure.CheckHostedServiceNameAvailability(name) 335 if err != nil { 336 // The calling function should retry. 337 return nil, nil 338 } 339 if label == "" { 340 label = name 341 } 342 req := gwacl.NewCreateHostedServiceWithLocation(name, label, "") 343 req.AffinityGroup = affinityGroupName 344 err = azure.AddHostedService(req) 345 if err != nil { 346 return nil, err 347 } 348 return req, nil 349 } 350 351 // newHostedService creates a hosted service. It will make up a unique name, 352 // starting with the given prefix. 353 func newHostedService(azure *gwacl.ManagementAPI, prefix, affinityGroupName, label string) (*gwacl.HostedService, error) { 354 var err error 355 var createdService *gwacl.CreateHostedService 356 for tries := 10; tries > 0 && err == nil && createdService == nil; tries-- { 357 createdService, err = attemptCreateService(azure, prefix, affinityGroupName, label) 358 } 359 if err != nil { 360 return nil, fmt.Errorf("could not create hosted service: %v", err) 361 } 362 if createdService == nil { 363 return nil, fmt.Errorf("could not come up with a unique hosted service name - is your randomizer initialized?") 364 } 365 return azure.GetHostedServiceProperties(createdService.ServiceName, true) 366 } 367 368 // SupportedArchitectures is specified on the EnvironCapability interface. 369 func (env *azureEnviron) SupportedArchitectures() ([]string, error) { 370 env.archMutex.Lock() 371 defer env.archMutex.Unlock() 372 if env.supportedArchitectures != nil { 373 return env.supportedArchitectures, nil 374 } 375 // Create a filter to get all images from our region and for the correct stream. 376 ecfg := env.getSnapshot().ecfg 377 region := ecfg.location() 378 cloudSpec := simplestreams.CloudSpec{ 379 Region: region, 380 Endpoint: getEndpoint(region), 381 } 382 imageConstraint := imagemetadata.NewImageConstraint(simplestreams.LookupParams{ 383 CloudSpec: cloudSpec, 384 Stream: ecfg.ImageStream(), 385 }) 386 var err error 387 env.supportedArchitectures, err = common.SupportedArchitectures(env, imageConstraint) 388 return env.supportedArchitectures, err 389 } 390 391 // SupportNetworks is specified on the EnvironCapability interface. 392 func (env *azureEnviron) SupportNetworks() bool { 393 return false 394 } 395 396 // selectInstanceTypeAndImage returns the appropriate instance-type name and 397 // the OS image name for launching a virtual machine with the given parameters. 398 func (env *azureEnviron) selectInstanceTypeAndImage(constraint *instances.InstanceConstraint) (string, string, error) { 399 ecfg := env.getSnapshot().ecfg 400 sourceImageName := ecfg.forceImageName() 401 if sourceImageName != "" { 402 // Configuration forces us to use a specific image. There may 403 // not be a suitable image in the simplestreams database. 404 // This means we can't use Juju's normal selection mechanism, 405 // because it combines instance-type and image selection: if 406 // there are no images we can use, it won't offer us an 407 // instance type either. 408 // 409 // Select the instance type using simple, Azure-specific code. 410 machineType, err := selectMachineType(gwacl.RoleSizes, defaultToBaselineSpec(constraint.Constraints)) 411 if err != nil { 412 return "", "", err 413 } 414 return machineType.Name, sourceImageName, nil 415 } 416 417 // Choose the most suitable instance type and OS image, based on simplestreams information. 418 spec, err := findInstanceSpec(env, constraint) 419 if err != nil { 420 return "", "", err 421 } 422 return spec.InstanceType.Id, spec.Image.Id, nil 423 } 424 425 var unsupportedConstraints = []string{ 426 constraints.CpuPower, 427 constraints.Tags, 428 } 429 430 // ConstraintsValidator is defined on the Environs interface. 431 func (env *azureEnviron) ConstraintsValidator() (constraints.Validator, error) { 432 validator := constraints.NewValidator() 433 validator.RegisterUnsupported(unsupportedConstraints) 434 supportedArches, err := env.SupportedArchitectures() 435 if err != nil { 436 return nil, err 437 } 438 validator.RegisterVocabulary(constraints.Arch, supportedArches) 439 instTypeNames := make([]string, len(gwacl.RoleSizes)) 440 for i, role := range gwacl.RoleSizes { 441 instTypeNames[i] = role.Name 442 } 443 validator.RegisterVocabulary(constraints.InstanceType, instTypeNames) 444 return validator, nil 445 } 446 447 // PrecheckInstance is defined on the state.Prechecker interface. 448 func (env *azureEnviron) PrecheckInstance(series string, cons constraints.Value, placement string) error { 449 if placement != "" { 450 return fmt.Errorf("unknown placement directive: %s", placement) 451 } 452 if !cons.HasInstanceType() { 453 return nil 454 } 455 // Constraint has an instance-type constraint so let's see if it is valid. 456 instanceTypes, err := listInstanceTypes(env, gwacl.RoleSizes) 457 if err != nil { 458 return err 459 } 460 for _, instanceType := range instanceTypes { 461 if instanceType.Name == *cons.InstanceType { 462 return nil 463 } 464 } 465 return fmt.Errorf("invalid Azure instance %q specified", *cons.InstanceType) 466 } 467 468 // createInstance creates all of the Azure entities necessary for a 469 // new instance. This includes Cloud Service, Deployment and Role. 470 // 471 // If serviceName is non-empty, then createInstance will assign to 472 // the Cloud Service with that name. Otherwise, a new Cloud Service 473 // will be created. 474 func (env *azureEnviron) createInstance(azure *gwacl.ManagementAPI, role *gwacl.Role, serviceName string, stateServer bool) (resultInst instance.Instance, resultErr error) { 475 var inst instance.Instance 476 defer func() { 477 if inst != nil && resultErr != nil { 478 if err := env.StopInstances(inst.Id()); err != nil { 479 // Failure upon failure. Log it, but return the original error. 480 logger.Errorf("error releasing failed instance: %v", err) 481 } 482 } 483 }() 484 var err error 485 var service *gwacl.HostedService 486 if serviceName != "" { 487 logger.Debugf("creating instance in existing cloud service %q", serviceName) 488 service, err = azure.GetHostedServiceProperties(serviceName, true) 489 } else { 490 logger.Debugf("creating instance in new cloud service") 491 // If we're creating a cloud service for state servers, 492 // we will want to open additional ports. We need to 493 // record this against the cloud service, so we use a 494 // special label for the purpose. 495 var label string 496 if stateServer { 497 label = stateServerLabel 498 } 499 service, err = newHostedService(azure, env.getEnvPrefix(), env.getAffinityGroupName(), label) 500 } 501 if err != nil { 502 return nil, err 503 } 504 if len(service.Deployments) == 0 { 505 // This is a newly created cloud service, so we 506 // should destroy it if anything below fails. 507 defer func() { 508 if resultErr != nil { 509 azure.DeleteHostedService(service.ServiceName) 510 // Destroying the hosted service destroys the instance, 511 // so ensure StopInstances isn't called. 512 inst = nil 513 } 514 }() 515 // Create an initial deployment. 516 deployment := gwacl.NewDeploymentForCreateVMDeployment( 517 deploymentNameV2(service.ServiceName), 518 deploymentSlot, 519 deploymentNameV2(service.ServiceName), 520 []gwacl.Role{*role}, 521 env.getVirtualNetworkName(), 522 ) 523 if err := azure.AddDeployment(deployment, service.ServiceName); err != nil { 524 return nil, err 525 } 526 service.Deployments = append(service.Deployments, *deployment) 527 } else { 528 // Update the deployment. 529 deployment := &service.Deployments[0] 530 if err := azure.AddRole(&gwacl.AddRoleRequest{ 531 ServiceName: service.ServiceName, 532 DeploymentName: deployment.Name, 533 PersistentVMRole: (*gwacl.PersistentVMRole)(role), 534 }); err != nil { 535 return nil, err 536 } 537 deployment.RoleList = append(deployment.RoleList, *role) 538 } 539 return env.getInstance(service, role.RoleName) 540 } 541 542 // deploymentNameV1 returns the deployment name used 543 // in the original implementation of the Azure provider. 544 func deploymentNameV1(serviceName string) string { 545 return serviceName 546 } 547 548 // deploymentNameV2 returns the deployment name used 549 // in the current implementation of the Azure provider. 550 func deploymentNameV2(serviceName string) string { 551 return serviceName + "-v2" 552 } 553 554 // StartInstance is specified in the InstanceBroker interface. 555 func (env *azureEnviron) StartInstance(args environs.StartInstanceParams) (_ instance.Instance, _ *instance.HardwareCharacteristics, _ []network.Info, err error) { 556 if args.MachineConfig.HasNetworks() { 557 return nil, nil, nil, fmt.Errorf("starting instances with networks is not supported yet.") 558 } 559 560 err = environs.FinishMachineConfig(args.MachineConfig, env.Config(), args.Constraints) 561 if err != nil { 562 return nil, nil, nil, err 563 } 564 565 // Pick envtools. Needed for the custom data (which is what we normally 566 // call userdata). 567 args.MachineConfig.Tools = args.Tools[0] 568 logger.Infof("picked tools %q", args.MachineConfig.Tools) 569 570 // Compose userdata. 571 userData, err := makeCustomData(args.MachineConfig) 572 if err != nil { 573 return nil, nil, nil, fmt.Errorf("custom data: %v", err) 574 } 575 576 azure, err := env.getManagementAPI() 577 if err != nil { 578 return nil, nil, nil, err 579 } 580 defer env.releaseManagementAPI(azure) 581 582 snapshot := env.getSnapshot() 583 location := snapshot.ecfg.location() 584 instanceType, sourceImageName, err := env.selectInstanceTypeAndImage(&instances.InstanceConstraint{ 585 Region: location, 586 Series: args.Tools.OneSeries(), 587 Arches: args.Tools.Arches(), 588 Constraints: args.Constraints, 589 }) 590 if err != nil { 591 return nil, nil, nil, err 592 } 593 594 // We use the cloud service label as a way to group instances with 595 // the same affinity, so that machines can be be allocated to the 596 // same availability set. 597 var cloudServiceName string 598 if args.DistributionGroup != nil && snapshot.ecfg.availabilitySetsEnabled() { 599 instanceIds, err := args.DistributionGroup() 600 if err != nil { 601 return nil, nil, nil, err 602 } 603 for _, id := range instanceIds { 604 cloudServiceName, _ = env.splitInstanceId(id) 605 if cloudServiceName != "" { 606 break 607 } 608 } 609 } 610 611 vhd := env.newOSDisk(sourceImageName) 612 // If we're creating machine-0, we'll want to expose port 22. 613 // All other machines get an auto-generated public port for SSH. 614 stateServer := false 615 for _, job := range args.MachineConfig.Jobs { 616 if job == params.JobManageEnviron { 617 stateServer = true 618 break 619 } 620 } 621 role := env.newRole(instanceType, vhd, userData, stateServer) 622 inst, err := createInstance(env, azure.ManagementAPI, role, cloudServiceName, stateServer) 623 if err != nil { 624 return nil, nil, nil, err 625 } 626 // TODO(bug 1193998) - return instance hardware characteristics as well 627 return inst, &instance.HardwareCharacteristics{}, nil, nil 628 } 629 630 // getInstance returns an up-to-date version of the instance with the given 631 // name. 632 func (env *azureEnviron) getInstance(hostedService *gwacl.HostedService, roleName string) (instance.Instance, error) { 633 if n := len(hostedService.Deployments); n != 1 { 634 return nil, fmt.Errorf("expected one deployment for %q, got %d", hostedService.ServiceName, n) 635 } 636 deployment := &hostedService.Deployments[0] 637 638 var maskStateServerPorts bool 639 var instanceId instance.Id 640 switch deployment.Name { 641 case deploymentNameV1(hostedService.ServiceName): 642 // Old style instance. 643 instanceId = instance.Id(hostedService.ServiceName) 644 if n := len(deployment.RoleList); n != 1 { 645 return nil, fmt.Errorf("expected one role for %q, got %d", deployment.Name, n) 646 } 647 roleName = deployment.RoleList[0].RoleName 648 // In the old implementation of the Azure provider, 649 // all machines opened the state and API server ports. 650 maskStateServerPorts = true 651 652 case deploymentNameV2(hostedService.ServiceName): 653 instanceId = instance.Id(fmt.Sprintf("%s-%s", hostedService.ServiceName, roleName)) 654 // Newly created state server machines are put into 655 // the cloud service with the stateServerLabel label. 656 if decoded, err := base64.StdEncoding.DecodeString(hostedService.Label); err == nil { 657 maskStateServerPorts = string(decoded) == stateServerLabel 658 } 659 } 660 661 var roleInstance *gwacl.RoleInstance 662 for _, role := range deployment.RoleInstanceList { 663 if role.RoleName == roleName { 664 roleInstance = &role 665 break 666 } 667 } 668 669 instance := &azureInstance{ 670 environ: env, 671 hostedService: &hostedService.HostedServiceDescriptor, 672 instanceId: instanceId, 673 deploymentName: deployment.Name, 674 roleName: roleName, 675 roleInstance: roleInstance, 676 maskStateServerPorts: maskStateServerPorts, 677 } 678 return instance, nil 679 } 680 681 // newOSDisk creates a gwacl.OSVirtualHardDisk object suitable for an 682 // Azure Virtual Machine. 683 func (env *azureEnviron) newOSDisk(sourceImageName string) *gwacl.OSVirtualHardDisk { 684 vhdName := gwacl.MakeRandomDiskName("juju") 685 vhdPath := fmt.Sprintf("vhds/%s", vhdName) 686 snap := env.getSnapshot() 687 storageAccount := snap.ecfg.storageAccountName() 688 mediaLink := gwacl.CreateVirtualHardDiskMediaLink(storageAccount, vhdPath) 689 // The disk label is optional and the disk name can be omitted if 690 // mediaLink is provided. 691 return gwacl.NewOSVirtualHardDisk("", "", "", mediaLink, sourceImageName, "Linux") 692 } 693 694 // getInitialEndpoints returns a slice of the endpoints every instance should have open 695 // (ssh port, etc). 696 func (env *azureEnviron) getInitialEndpoints(stateServer bool) []gwacl.InputEndpoint { 697 // TODO(axw) either proxy ssh traffic through one of the 698 // randomly chosen VMs to the internal address, or otherwise 699 // don't load balance SSH and provide a way of getting the 700 // local port. 701 cfg := env.Config() 702 endpoints := []gwacl.InputEndpoint{{ 703 LocalPort: 22, 704 Name: "sshport", 705 Port: 22, 706 Protocol: "tcp", 707 }} 708 if stateServer { 709 endpoints = append(endpoints, []gwacl.InputEndpoint{{ 710 LocalPort: cfg.StatePort(), 711 Port: cfg.StatePort(), 712 Protocol: "tcp", 713 Name: "stateport", 714 }, { 715 LocalPort: cfg.APIPort(), 716 Port: cfg.APIPort(), 717 Protocol: "tcp", 718 Name: "apiport", 719 }}...) 720 } 721 for i, endpoint := range endpoints { 722 endpoint.LoadBalancedEndpointSetName = endpoint.Name 723 endpoint.LoadBalancerProbe = &gwacl.LoadBalancerProbe{ 724 Port: endpoint.Port, 725 Protocol: "TCP", 726 } 727 endpoints[i] = endpoint 728 } 729 return endpoints 730 } 731 732 // newRole creates a gwacl.Role object (an Azure Virtual Machine) which uses 733 // the given Virtual Hard Drive. 734 // 735 // The VM will have: 736 // - an 'ubuntu' user defined with an unguessable (randomly generated) password 737 // - its ssh port (TCP 22) open 738 // (if a state server) 739 // - its state port (TCP mongoDB) port open 740 // - its API port (TCP) open 741 // 742 // roleSize is the name of one of Azure's machine types, e.g. ExtraSmall, 743 // Large, A6 etc. 744 func (env *azureEnviron) newRole(roleSize string, vhd *gwacl.OSVirtualHardDisk, userData string, stateServer bool) *gwacl.Role { 745 roleName := gwacl.MakeRandomRoleName("juju") 746 // Create a Linux Configuration with the username and the password 747 // empty and disable SSH with password authentication. 748 hostname := roleName 749 username := "ubuntu" 750 password := gwacl.MakeRandomPassword() 751 linuxConfigurationSet := gwacl.NewLinuxProvisioningConfigurationSet(hostname, username, password, userData, "true") 752 // Generate a Network Configuration with the initially required ports open. 753 networkConfigurationSet := gwacl.NewNetworkConfigurationSet(env.getInitialEndpoints(stateServer), nil) 754 role := gwacl.NewRole( 755 roleSize, roleName, vhd, 756 []gwacl.ConfigurationSet{*linuxConfigurationSet, *networkConfigurationSet}, 757 ) 758 role.AvailabilitySetName = "juju" 759 return role 760 } 761 762 // StopInstances is specified in the InstanceBroker interface. 763 func (env *azureEnviron) StopInstances(ids ...instance.Id) error { 764 context, err := env.getManagementAPI() 765 if err != nil { 766 return err 767 } 768 defer env.releaseManagementAPI(context) 769 770 // Map services to role names we want to delete. 771 serviceInstances := make(map[string]map[string]bool) 772 var serviceNames []string 773 for _, id := range ids { 774 serviceName, roleName := env.splitInstanceId(id) 775 if roleName == "" { 776 serviceInstances[serviceName] = nil 777 serviceNames = append(serviceNames, serviceName) 778 } else { 779 deleteRoleNames, ok := serviceInstances[serviceName] 780 if !ok { 781 deleteRoleNames = make(map[string]bool) 782 serviceInstances[serviceName] = deleteRoleNames 783 serviceNames = append(serviceNames, serviceName) 784 } 785 deleteRoleNames[roleName] = true 786 } 787 } 788 789 // Load the properties of each service, so we know whether to 790 // delete the entire service. 791 // 792 // Note: concurrent operations on Affinity Groups have been 793 // found to cause conflict responses, so we do everything serially. 794 for _, serviceName := range serviceNames { 795 deleteRoleNames := serviceInstances[serviceName] 796 service, err := context.GetHostedServiceProperties(serviceName, true) 797 if err != nil { 798 return err 799 } else if len(service.Deployments) != 1 { 800 continue 801 } 802 // Filter the instances that have no corresponding role. 803 var roleNames set.Strings 804 for _, role := range service.Deployments[0].RoleList { 805 roleNames.Add(role.RoleName) 806 } 807 for roleName := range deleteRoleNames { 808 if !roleNames.Contains(roleName) { 809 delete(deleteRoleNames, roleName) 810 } 811 } 812 // If we're deleting all the roles, we need to delete the 813 // entire cloud service or we'll get an error. deleteRoleNames 814 // is nil if we're dealing with a legacy deployment. 815 if deleteRoleNames == nil || len(deleteRoleNames) == roleNames.Size() { 816 if err := context.DeleteHostedService(serviceName); err != nil { 817 return err 818 } 819 } else { 820 for roleName := range deleteRoleNames { 821 if err := context.DeleteRole(&gwacl.DeleteRoleRequest{ 822 ServiceName: serviceName, 823 DeploymentName: service.Deployments[0].Name, 824 RoleName: roleName, 825 DeleteMedia: true, 826 }); err != nil { 827 return err 828 } 829 } 830 } 831 } 832 return nil 833 } 834 835 // destroyAllServices destroys all Cloud Services and deployments contained. 836 // This is needed to clean up broken environments, in which there are cloud 837 // services with no deployments. 838 func (env *azureEnviron) destroyAllServices() error { 839 context, err := env.getManagementAPI() 840 if err != nil { 841 return err 842 } 843 defer env.releaseManagementAPI(context) 844 845 request := &gwacl.ListPrefixedHostedServicesRequest{ServiceNamePrefix: env.getEnvPrefix()} 846 services, err := context.ListPrefixedHostedServices(request) 847 if err != nil { 848 return err 849 } 850 for _, service := range services { 851 if err := context.DeleteHostedService(service.ServiceName); err != nil { 852 return err 853 } 854 } 855 return nil 856 } 857 858 // splitInstanceId splits the specified instance.Id into its 859 // cloud-service and role parts. Both values will be empty 860 // if the instance-id is non-matching, and role will be empty 861 // for legacy instance-ids. 862 func (env *azureEnviron) splitInstanceId(id instance.Id) (service, role string) { 863 prefix := env.getEnvPrefix() 864 if !strings.HasPrefix(string(id), prefix) { 865 return "", "" 866 } 867 fields := strings.Split(string(id)[len(prefix):], "-") 868 service = prefix + fields[0] 869 if len(fields) > 1 { 870 role = fields[1] 871 } 872 return service, role 873 } 874 875 // Instances is specified in the Environ interface. 876 func (env *azureEnviron) Instances(ids []instance.Id) ([]instance.Instance, error) { 877 context, err := env.getManagementAPI() 878 if err != nil { 879 return nil, err 880 } 881 defer env.releaseManagementAPI(context) 882 883 type instanceId struct { 884 serviceName, roleName string 885 } 886 887 instancesIds := make([]instanceId, len(ids)) 888 var serviceNames set.Strings 889 for i, id := range ids { 890 serviceName, roleName := env.splitInstanceId(id) 891 if serviceName == "" { 892 continue 893 } 894 instancesIds[i] = instanceId{ 895 serviceName: serviceName, 896 roleName: roleName, 897 } 898 serviceNames.Add(serviceName) 899 } 900 901 // Map service names to gwacl.HostedServices. 902 services, err := context.ListSpecificHostedServices(&gwacl.ListSpecificHostedServicesRequest{ 903 ServiceNames: serviceNames.Values(), 904 }) 905 if err != nil { 906 return nil, err 907 } 908 if len(services) == 0 { 909 return nil, environs.ErrNoInstances 910 } 911 hostedServices := make(map[string]*gwacl.HostedService) 912 for _, s := range services { 913 hostedService, err := context.GetHostedServiceProperties(s.ServiceName, true) 914 if err != nil { 915 return nil, err 916 } 917 hostedServices[s.ServiceName] = hostedService 918 } 919 920 err = nil 921 instances := make([]instance.Instance, len(ids)) 922 for i, id := range instancesIds { 923 if id.serviceName == "" { 924 // Previously determined to be an invalid instance ID. 925 continue 926 } 927 hostedService := hostedServices[id.serviceName] 928 instance, err := env.getInstance(hostedService, id.roleName) 929 if err == nil { 930 instances[i] = instance 931 } else { 932 logger.Debugf("failed to get instance for role %q in service %q: %v", id.roleName, hostedService.ServiceName, err) 933 } 934 } 935 for _, instance := range instances { 936 if instance == nil { 937 err = environs.ErrPartialInstances 938 } 939 } 940 return instances, err 941 } 942 943 // AllocateAddress requests a new address to be allocated for the 944 // given instance on the given network. This is not implemented on the 945 // Azure provider yet. 946 func (*azureEnviron) AllocateAddress(_ instance.Id, _ network.Id) (instance.Address, error) { 947 return instance.Address{}, errors.NotImplementedf("AllocateAddress") 948 } 949 950 // AllInstances is specified in the InstanceBroker interface. 951 func (env *azureEnviron) AllInstances() ([]instance.Instance, error) { 952 // The instance list is built using the list of all the Azure 953 // Services (instance==service). 954 // Acquire management API object. 955 context, err := env.getManagementAPI() 956 if err != nil { 957 return nil, err 958 } 959 defer env.releaseManagementAPI(context) 960 961 request := &gwacl.ListPrefixedHostedServicesRequest{ServiceNamePrefix: env.getEnvPrefix()} 962 serviceDescriptors, err := context.ListPrefixedHostedServices(request) 963 if err != nil { 964 return nil, err 965 } 966 967 var instances []instance.Instance 968 for _, sd := range serviceDescriptors { 969 hostedService, err := context.GetHostedServiceProperties(sd.ServiceName, true) 970 if err != nil { 971 return nil, err 972 } else if len(hostedService.Deployments) != 1 { 973 continue 974 } 975 deployment := &hostedService.Deployments[0] 976 for _, role := range deployment.RoleList { 977 instance, err := env.getInstance(hostedService, role.RoleName) 978 if err != nil { 979 return nil, err 980 } 981 instances = append(instances, instance) 982 } 983 } 984 return instances, nil 985 } 986 987 // getEnvPrefix returns the prefix used to name the objects specific to this 988 // environment. 989 func (env *azureEnviron) getEnvPrefix() string { 990 return fmt.Sprintf("juju-%s-", env.Name()) 991 } 992 993 // Storage is specified in the Environ interface. 994 func (env *azureEnviron) Storage() storage.Storage { 995 return env.getSnapshot().storage 996 } 997 998 // Destroy is specified in the Environ interface. 999 func (env *azureEnviron) Destroy() error { 1000 logger.Debugf("destroying environment %q", env.name) 1001 1002 // Stop all instances. 1003 if err := env.destroyAllServices(); err != nil { 1004 return fmt.Errorf("cannot destroy instances: %v", err) 1005 } 1006 1007 // Delete vnet and affinity group. 1008 if err := env.deleteVirtualNetwork(); err != nil { 1009 return fmt.Errorf("cannot delete the environment's virtual network: %v", err) 1010 } 1011 if err := env.deleteAffinityGroup(); err != nil { 1012 return fmt.Errorf("cannot delete the environment's affinity group: %v", err) 1013 } 1014 1015 // Delete storage. 1016 // Deleting the storage is done last so that if something fails 1017 // half way through the Destroy() method, the storage won't be cleaned 1018 // up and thus an attempt to re-boostrap the environment will lead to 1019 // a "error: environment is already bootstrapped" error. 1020 if err := env.Storage().RemoveAll(); err != nil { 1021 return fmt.Errorf("cannot clean up storage: %v", err) 1022 } 1023 return nil 1024 } 1025 1026 // OpenPorts is specified in the Environ interface. However, Azure does not 1027 // support the global firewall mode. 1028 func (env *azureEnviron) OpenPorts(ports []instance.Port) error { 1029 return nil 1030 } 1031 1032 // ClosePorts is specified in the Environ interface. However, Azure does not 1033 // support the global firewall mode. 1034 func (env *azureEnviron) ClosePorts(ports []instance.Port) error { 1035 return nil 1036 } 1037 1038 // Ports is specified in the Environ interface. 1039 func (env *azureEnviron) Ports() ([]instance.Port, error) { 1040 // TODO: implement this. 1041 return []instance.Port{}, nil 1042 } 1043 1044 // Provider is specified in the Environ interface. 1045 func (env *azureEnviron) Provider() environs.EnvironProvider { 1046 return azureEnvironProvider{} 1047 } 1048 1049 // azureManagementContext wraps two things: a gwacl.ManagementAPI (effectively 1050 // a session on the Azure management API) and a tempCertFile, which keeps track 1051 // of the temporary certificate file that needs to be deleted once we're done 1052 // with this particular session. 1053 // Since it embeds *gwacl.ManagementAPI, you can use it much as if it were a 1054 // pointer to a ManagementAPI object. Just don't forget to release it after 1055 // use. 1056 type azureManagementContext struct { 1057 *gwacl.ManagementAPI 1058 certFile *tempCertFile 1059 } 1060 1061 var ( 1062 retryPolicy = gwacl.RetryPolicy{ 1063 NbRetries: 6, 1064 HttpStatusCodes: []int{ 1065 http.StatusConflict, 1066 http.StatusRequestTimeout, 1067 http.StatusInternalServerError, 1068 http.StatusServiceUnavailable, 1069 }, 1070 Delay: 10 * time.Second} 1071 ) 1072 1073 // getManagementAPI obtains a context object for interfacing with Azure's 1074 // management API. 1075 // For now, each invocation just returns a separate object. This is probably 1076 // wasteful (each context gets its own SSL connection) and may need optimizing 1077 // later. 1078 func (env *azureEnviron) getManagementAPI() (*azureManagementContext, error) { 1079 snap := env.getSnapshot() 1080 subscription := snap.ecfg.managementSubscriptionId() 1081 certData := snap.ecfg.managementCertificate() 1082 certFile, err := newTempCertFile([]byte(certData)) 1083 if err != nil { 1084 return nil, err 1085 } 1086 // After this point, if we need to leave prematurely, we should clean 1087 // up that certificate file. 1088 location := snap.ecfg.location() 1089 mgtAPI, err := gwacl.NewManagementAPIWithRetryPolicy(subscription, certFile.Path(), location, retryPolicy) 1090 if err != nil { 1091 certFile.Delete() 1092 return nil, err 1093 } 1094 context := azureManagementContext{ 1095 ManagementAPI: mgtAPI, 1096 certFile: certFile, 1097 } 1098 return &context, nil 1099 } 1100 1101 // releaseManagementAPI frees up a context object obtained through 1102 // getManagementAPI. 1103 func (env *azureEnviron) releaseManagementAPI(context *azureManagementContext) { 1104 // Be tolerant to incomplete context objects, in case we ever get 1105 // called during cleanup of a failed attempt to create one. 1106 if context == nil || context.certFile == nil { 1107 return 1108 } 1109 // For now, all that needs doing is to delete the temporary certificate 1110 // file. We may do cleverer things later, such as connection pooling 1111 // where this method returns a context to the pool. 1112 context.certFile.Delete() 1113 } 1114 1115 // updateStorageAccountKey queries the storage account key, and updates the 1116 // version cached in env.storageAccountKey. 1117 // 1118 // It takes a snapshot in order to preserve transactional integrity relative 1119 // to the snapshot's starting state, without having to lock the environment 1120 // for the duration. If there is a conflicting change to env relative to the 1121 // state recorded in the snapshot, this function will fail. 1122 func (env *azureEnviron) updateStorageAccountKey(snapshot *azureEnviron) (string, error) { 1123 // This method follows an RCU pattern, an optimistic technique to 1124 // implement atomic read-update transactions: get a consistent snapshot 1125 // of state; process data; enter critical section; check for conflicts; 1126 // write back changes. The advantage is that there are no long-held 1127 // locks, in particular while waiting for the request to Azure to 1128 // complete. 1129 // "Get a consistent snapshot of state" is the caller's responsibility. 1130 // The caller can use env.getSnapshot(). 1131 1132 // Process data: get a current account key from Azure. 1133 key, err := env.queryStorageAccountKey() 1134 if err != nil { 1135 return "", err 1136 } 1137 1138 // Enter critical section. 1139 env.Lock() 1140 defer env.Unlock() 1141 1142 // Check for conflicts: is the config still what it was? 1143 if env.ecfg != snapshot.ecfg { 1144 // The environment has been reconfigured while we were 1145 // working on this, so the key we just get may not be 1146 // appropriate any longer. So fail. 1147 // Whatever we were doing isn't likely to be right any more 1148 // anyway. Otherwise, it might be worth returning the key 1149 // just in case it still works, and proceed without updating 1150 // env.storageAccountKey. 1151 return "", fmt.Errorf("environment was reconfigured") 1152 } 1153 1154 // Write back changes. 1155 env.storageAccountKey = key 1156 return key, nil 1157 } 1158 1159 // getStorageContext obtains a context object for interfacing with Azure's 1160 // storage API. 1161 // For now, each invocation just returns a separate object. This is probably 1162 // wasteful (each context gets its own SSL connection) and may need optimizing 1163 // later. 1164 func (env *azureEnviron) getStorageContext() (*gwacl.StorageContext, error) { 1165 snap := env.getSnapshot() 1166 key := snap.storageAccountKey 1167 if key == "" { 1168 // We don't know the storage-account key yet. Request it. 1169 var err error 1170 key, err = env.updateStorageAccountKey(snap) 1171 if err != nil { 1172 return nil, err 1173 } 1174 } 1175 context := gwacl.StorageContext{ 1176 Account: snap.ecfg.storageAccountName(), 1177 Key: key, 1178 AzureEndpoint: gwacl.GetEndpoint(snap.ecfg.location()), 1179 RetryPolicy: retryPolicy, 1180 } 1181 return &context, nil 1182 } 1183 1184 // baseURLs specifies an Azure specific location where we look for simplestreams information. 1185 // It contains the central databases for the released and daily streams, but this may 1186 // become more configurable. This variable is here as a placeholder, but also 1187 // as an injection point for tests. 1188 var baseURLs = []string{} 1189 1190 // GetImageSources returns a list of sources which are used to search for simplestreams image metadata. 1191 func (env *azureEnviron) GetImageSources() ([]simplestreams.DataSource, error) { 1192 sources := make([]simplestreams.DataSource, 1+len(baseURLs)) 1193 sources[0] = storage.NewStorageSimpleStreamsDataSource("cloud storage", env.Storage(), storage.BaseImagesPath) 1194 for i, url := range baseURLs { 1195 sources[i+1] = simplestreams.NewURLDataSource("Azure base URL", url, utils.VerifySSLHostnames) 1196 } 1197 return sources, nil 1198 } 1199 1200 // GetToolsSources returns a list of sources which are used to search for simplestreams tools metadata. 1201 func (env *azureEnviron) GetToolsSources() ([]simplestreams.DataSource, error) { 1202 // Add the simplestreams source off the control bucket. 1203 sources := []simplestreams.DataSource{ 1204 storage.NewStorageSimpleStreamsDataSource("cloud storage", env.Storage(), storage.BaseToolsPath)} 1205 return sources, nil 1206 } 1207 1208 // getImageMetadataSigningRequired returns whether this environment requires 1209 // image metadata from Simplestreams to be signed. 1210 func (env *azureEnviron) getImageMetadataSigningRequired() bool { 1211 // Hard-coded to true for now. Once we support custom base URLs, 1212 // this may have to change. 1213 return true 1214 } 1215 1216 // Region is specified in the HasRegion interface. 1217 func (env *azureEnviron) Region() (simplestreams.CloudSpec, error) { 1218 ecfg := env.getSnapshot().ecfg 1219 return simplestreams.CloudSpec{ 1220 Region: ecfg.location(), 1221 Endpoint: string(gwacl.GetEndpoint(ecfg.location())), 1222 }, nil 1223 } 1224 1225 // SupportsUnitPlacement is specified in the state.EnvironCapability interface. 1226 func (env *azureEnviron) SupportsUnitPlacement() error { 1227 if env.getSnapshot().ecfg.availabilitySetsEnabled() { 1228 return fmt.Errorf("unit placement is not supported with availability-sets-enabled") 1229 } 1230 return nil 1231 }