github.com/cloud-green/juju@v0.0.0-20151002100041-a00291338d3d/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 "bytes" 8 "encoding/base64" 9 "encoding/json" 10 "fmt" 11 "net/http" 12 "regexp" 13 "strings" 14 "sync" 15 "time" 16 17 "github.com/juju/errors" 18 "github.com/juju/utils" 19 jujuos "github.com/juju/utils/os" 20 "github.com/juju/utils/series" 21 "github.com/juju/utils/set" 22 "launchpad.net/gwacl" 23 24 "github.com/juju/juju/cloudconfig/instancecfg" 25 "github.com/juju/juju/cloudconfig/providerinit" 26 "github.com/juju/juju/constraints" 27 "github.com/juju/juju/environs" 28 "github.com/juju/juju/environs/config" 29 "github.com/juju/juju/environs/imagemetadata" 30 "github.com/juju/juju/environs/instances" 31 "github.com/juju/juju/environs/simplestreams" 32 "github.com/juju/juju/environs/storage" 33 "github.com/juju/juju/instance" 34 "github.com/juju/juju/network" 35 "github.com/juju/juju/provider/common" 36 "github.com/juju/juju/state" 37 "github.com/juju/juju/state/multiwatcher" 38 ) 39 40 const ( 41 // deploymentSlot says in which slot to deploy instances. Azure 42 // supports 'Production' or 'Staging'. 43 // This provider always deploys to Production. Think twice about 44 // changing that: DNS names in the staging slot work differently from 45 // those in the production slot. In Staging, Azure assigns an 46 // arbitrary hostname that we can then extract from the deployment's 47 // URL. In Production, the hostname in the deployment URL does not 48 // actually seem to resolve; instead, the service name is used as the 49 // DNS name, with ".cloudapp.net" appended. 50 deploymentSlot = "Production" 51 52 // Address space of the virtual network used by the nodes in this 53 // environement, in CIDR notation. This is the network used for 54 // machine-to-machine communication. 55 networkDefinition = "10.0.0.0/8" 56 57 // stateServerLabel is the label applied to the cloud service created 58 // for state servers. 59 stateServerLabel = "juju-state-server" 60 ) 61 62 // vars for testing purposes. 63 var ( 64 createInstance = (*azureEnviron).createInstance 65 ) 66 67 type azureEnviron struct { 68 // Except where indicated otherwise, all fields in this object should 69 // only be accessed using a lock or a snapshot. 70 sync.Mutex 71 72 // archMutex gates access to supportedArchitectures 73 archMutex sync.Mutex 74 // supportedArchitectures caches the architectures 75 // for which images can be instantiated. 76 supportedArchitectures []string 77 78 // ecfg is the environment's Azure-specific configuration. 79 ecfg *azureEnvironConfig 80 81 // storage is this environ's own private storage. 82 storage storage.Storage 83 84 // storageAccountKey holds an access key to this environment's 85 // private storage. This is automatically queried from Azure on 86 // startup. 87 storageAccountKey string 88 89 // api is a management API for Microsoft Azure. 90 api *gwacl.ManagementAPI 91 92 // vnet describes the configured virtual network. 93 vnet *gwacl.VirtualNetworkSite 94 95 // availableRoleSizes is the role sizes available in the configured 96 // location. This will be reset whenever the location configuration changes. 97 availableRoleSizes set.Strings 98 } 99 100 // azureEnviron implements Environ and HasRegion. 101 var _ environs.Environ = (*azureEnviron)(nil) 102 var _ simplestreams.HasRegion = (*azureEnviron)(nil) 103 var _ state.Prechecker = (*azureEnviron)(nil) 104 105 // NewEnviron creates a new azureEnviron. 106 func NewEnviron(cfg *config.Config) (*azureEnviron, error) { 107 var env azureEnviron 108 err := env.SetConfig(cfg) 109 if err != nil { 110 return nil, err 111 } 112 113 // Set up storage. 114 env.storage = &azureStorage{ 115 storageContext: &environStorageContext{environ: &env}, 116 } 117 return &env, nil 118 } 119 120 // extractStorageKey returns the primary account key from a gwacl 121 // StorageAccountKeys struct, or if there is none, the secondary one. 122 func extractStorageKey(keys *gwacl.StorageAccountKeys) string { 123 if keys.Primary != "" { 124 return keys.Primary 125 } 126 return keys.Secondary 127 } 128 129 // queryStorageAccountKey retrieves the storage account's key from Azure. 130 func (env *azureEnviron) queryStorageAccountKey() (string, error) { 131 snap := env.getSnapshot() 132 133 accountName := snap.ecfg.storageAccountName() 134 keys, err := snap.api.GetStorageAccountKeys(accountName) 135 if err != nil { 136 return "", errors.Annotate(err, "cannot obtain storage account keys") 137 } 138 139 key := extractStorageKey(keys) 140 if key == "" { 141 return "", errors.New("no keys available for storage account") 142 } 143 144 return key, nil 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. The affinity group name is immutable, 168 // so there is no need to use a configuration snapshot. 169 func (env *azureEnviron) getAffinityGroupName() string { 170 return env.getEnvPrefix() + "ag" 171 } 172 173 // getLocation gets the configured Location for the environment. 174 func (env *azureEnviron) getLocation() string { 175 snap := env.getSnapshot() 176 return snap.ecfg.location() 177 } 178 179 func (env *azureEnviron) createAffinityGroup() error { 180 snap := env.getSnapshot() 181 affinityGroupName := env.getAffinityGroupName() 182 location := snap.ecfg.location() 183 cag := gwacl.NewCreateAffinityGroup(affinityGroupName, affinityGroupName, affinityGroupName, location) 184 return snap.api.CreateAffinityGroup(&gwacl.CreateAffinityGroupRequest{ 185 CreateAffinityGroup: cag, 186 }) 187 } 188 189 func (env *azureEnviron) deleteAffinityGroup() error { 190 snap := env.getSnapshot() 191 affinityGroupName := env.getAffinityGroupName() 192 return snap.api.DeleteAffinityGroup(&gwacl.DeleteAffinityGroupRequest{ 193 Name: affinityGroupName, 194 }) 195 } 196 197 // getAvailableRoleSizes returns the role sizes available for the configured 198 // location. 199 func (env *azureEnviron) getAvailableRoleSizes() (_ set.Strings, err error) { 200 defer errors.DeferredAnnotatef(&err, "cannot get available role sizes") 201 202 snap := env.getSnapshot() 203 if snap.availableRoleSizes != nil { 204 return snap.availableRoleSizes, nil 205 } 206 locations, err := snap.api.ListLocations() 207 if err != nil { 208 return nil, errors.Annotate(err, "cannot list locations") 209 } 210 var available set.Strings 211 for _, location := range locations { 212 if location.Name != snap.ecfg.location() { 213 continue 214 } 215 if location.ComputeCapabilities == nil { 216 return nil, errors.Annotate(err, "cannot determine compute capabilities") 217 } 218 available = set.NewStrings(location.ComputeCapabilities.VirtualMachineRoleSizes...) 219 break 220 } 221 if available == nil { 222 return nil, errors.NotFoundf("location %q", snap.ecfg.location()) 223 } 224 env.Lock() 225 env.availableRoleSizes = available 226 env.Unlock() 227 return available, nil 228 } 229 230 // getVirtualNetworkName returns the name of the virtual network used by all 231 // the VMs in this environment. The virtual network name is immutable, 232 // so there is no need to use a configuration snapshot. 233 func (env *azureEnviron) getVirtualNetworkName() string { 234 return env.getEnvPrefix() + "vnet" 235 } 236 237 // getVirtualNetwork returns the virtual network used by all the VMs in this 238 // environment. 239 func (env *azureEnviron) getVirtualNetwork() (*gwacl.VirtualNetworkSite, error) { 240 snap := env.getSnapshot() 241 if snap.vnet != nil { 242 return snap.vnet, nil 243 } 244 cfg, err := env.api.GetNetworkConfiguration() 245 if err != nil { 246 return nil, errors.Annotate(err, "error getting network configuration") 247 } 248 var vnet *gwacl.VirtualNetworkSite 249 vnetName := env.getVirtualNetworkName() 250 if cfg != nil && cfg.VirtualNetworkSites != nil { 251 for _, site := range *cfg.VirtualNetworkSites { 252 if site.Name == vnetName { 253 vnet = &site 254 break 255 } 256 } 257 } 258 if vnet == nil { 259 return nil, errors.NotFoundf("virtual network %q", vnetName) 260 } 261 env.Lock() 262 env.vnet = vnet 263 env.Unlock() 264 return vnet, nil 265 } 266 267 // createVirtualNetwork creates a virtual network for the environment. 268 func (env *azureEnviron) createVirtualNetwork() error { 269 snap := env.getSnapshot() 270 vnetName := env.getVirtualNetworkName() 271 virtualNetwork := gwacl.VirtualNetworkSite{ 272 Name: vnetName, 273 Location: snap.ecfg.location(), 274 AddressSpacePrefixes: []string{ 275 networkDefinition, 276 }, 277 } 278 if err := snap.api.AddVirtualNetworkSite(&virtualNetwork); err != nil { 279 return errors.Trace(err) 280 } 281 env.Lock() 282 env.vnet = &virtualNetwork 283 env.Unlock() 284 return nil 285 } 286 287 // deleteVnetAttempt is an AttemptyStrategy for use 288 // when attempting delete a virtual network. This is 289 // necessary as Azure apparently does not release all 290 // references to the vnet even when all cloud services 291 // are deleted. 292 var deleteVnetAttempt = utils.AttemptStrategy{ 293 Total: 30 * time.Second, 294 Delay: 1 * time.Second, 295 } 296 297 var networkInUse = regexp.MustCompile(".*The virtual network .* is currently in use.*") 298 299 func (env *azureEnviron) deleteVirtualNetwork() error { 300 snap := env.getSnapshot() 301 vnetName := env.getVirtualNetworkName() 302 var err error 303 for a := deleteVnetAttempt.Start(); a.Next(); { 304 err = snap.api.RemoveVirtualNetworkSite(vnetName) 305 if err == nil { 306 return nil 307 } 308 if err, ok := err.(*gwacl.AzureError); ok { 309 if err.StatusCode() == 400 && networkInUse.MatchString(err.Message) { 310 // Retry on "virtual network XYZ is currently in use". 311 continue 312 } 313 } 314 // Any other error should be returned. 315 break 316 } 317 return err 318 } 319 320 // getContainerName returns the name of the private storage account container 321 // that this environment is using. The container name is immutable, 322 // so there is no need to use a configuration snapshot. 323 func (env *azureEnviron) getContainerName() string { 324 return env.getEnvPrefix() + "private" 325 } 326 327 func isHTTPConflict(err error) bool { 328 if err, ok := err.(gwacl.HTTPError); ok { 329 return err.StatusCode() == http.StatusConflict 330 } 331 return false 332 } 333 334 func isVirtualNetworkExist(err error) bool { 335 // TODO(axw) 2014-06-16 #1330473 336 // Add an error type to gwacl for this. 337 s := err.Error() 338 const prefix = "could not add virtual network" 339 const suffix = "already exists" 340 return strings.HasPrefix(s, prefix) && strings.HasSuffix(s, suffix) 341 } 342 343 // Bootstrap is specified in the Environ interface. 344 func (env *azureEnviron) Bootstrap(ctx environs.BootstrapContext, args environs.BootstrapParams) (arch, series string, _ environs.BootstrapFinalizer, err error) { 345 // The creation of the affinity group and the virtual network is specific to the Azure provider. 346 err = env.createAffinityGroup() 347 if err != nil && !isHTTPConflict(err) { 348 return "", "", nil, err 349 } 350 // If we fail after this point, clean up the affinity group. 351 defer func() { 352 if err != nil { 353 env.deleteAffinityGroup() 354 } 355 }() 356 357 err = env.createVirtualNetwork() 358 if err != nil && !isVirtualNetworkExist(err) { 359 return "", "", nil, err 360 } 361 // If we fail after this point, clean up the virtual network. 362 defer func() { 363 if err != nil { 364 env.deleteVirtualNetwork() 365 } 366 }() 367 return common.Bootstrap(ctx, env, args) 368 } 369 370 // isLegacyInstance reports whether the instance is a 371 // legacy instance (i.e. one-to-one cloud service to instance). 372 func isLegacyInstance(inst *azureInstance) (bool, error) { 373 snap := inst.environ.getSnapshot() 374 serviceName := inst.hostedService.ServiceName 375 service, err := snap.api.GetHostedServiceProperties(serviceName, true) 376 if err != nil { 377 return false, err 378 } else if len(service.Deployments) != 1 { 379 return false, nil 380 } 381 deploymentName := service.Deployments[0].Name 382 return deploymentName == deploymentNameV1(serviceName), nil 383 } 384 385 // StateServerInstances is specified in the Environ interface. 386 func (env *azureEnviron) StateServerInstances() ([]instance.Id, error) { 387 // Locate the state-server cloud service, and get its addresses. 388 instances, err := env.AllInstances() 389 if err != nil { 390 return nil, err 391 } 392 var stateServerInstanceIds []instance.Id 393 var loadStateFile bool 394 for _, inst := range instances { 395 azureInstance := inst.(*azureInstance) 396 label := azureInstance.hostedService.Label 397 if decoded, err := base64.StdEncoding.DecodeString(label); err == nil { 398 if string(decoded) == stateServerLabel { 399 stateServerInstanceIds = append(stateServerInstanceIds, inst.Id()) 400 continue 401 } 402 } 403 if !loadStateFile { 404 _, roleName := env.splitInstanceId(azureInstance.Id()) 405 if roleName == "" { 406 loadStateFile = true 407 } 408 } 409 } 410 if loadStateFile { 411 // Some legacy instances were found, so we must load provider-state 412 // to find which instance was the original state server. If we find 413 // a legacy environment, then stateServerInstanceIds will not contain 414 // the original bootstrap instance, which is the only one that will 415 // be in provider-state. 416 instanceIds, err := common.ProviderStateInstances(env, env.Storage()) 417 if err != nil { 418 return nil, err 419 } 420 stateServerInstanceIds = append(stateServerInstanceIds, instanceIds...) 421 } 422 if len(stateServerInstanceIds) == 0 { 423 return nil, environs.ErrNoInstances 424 } 425 return stateServerInstanceIds, nil 426 } 427 428 // Config is specified in the Environ interface. 429 func (env *azureEnviron) Config() *config.Config { 430 snap := env.getSnapshot() 431 return snap.ecfg.Config 432 } 433 434 // SetConfig is specified in the Environ interface. 435 func (env *azureEnviron) SetConfig(cfg *config.Config) error { 436 var oldLocation string 437 if snap := env.getSnapshot(); snap.ecfg != nil { 438 oldLocation = snap.ecfg.location() 439 } 440 441 ecfg, err := azureEnvironProvider{}.newConfig(cfg) 442 if err != nil { 443 return err 444 } 445 446 env.Lock() 447 defer env.Unlock() 448 449 if env.ecfg != nil { 450 _, err = azureEnvironProvider{}.Validate(cfg, env.ecfg.Config) 451 if err != nil { 452 return err 453 } 454 } 455 456 env.ecfg = ecfg 457 458 // Reset storage account key. Even if we had one before, it may not 459 // be appropriate for the new config. 460 env.storageAccountKey = "" 461 462 subscription := ecfg.managementSubscriptionId() 463 certKeyPEM := []byte(ecfg.managementCertificate()) 464 location := ecfg.location() 465 mgtAPI, err := gwacl.NewManagementAPICertDataWithRetryPolicy(subscription, certKeyPEM, certKeyPEM, location, retryPolicy) 466 if err != nil { 467 return errors.Annotate(err, "cannot acquire management API") 468 } 469 env.api = mgtAPI 470 471 // If the location changed, reset the available role sizes. 472 if location != oldLocation { 473 env.availableRoleSizes = nil 474 } 475 476 return nil 477 } 478 479 // attemptCreateService tries to create a new hosted service on Azure, with a 480 // name it chooses (based on the given prefix), but recognizes that the name 481 // may not be available. If the name is not available, it does not treat that 482 // as an error but just returns nil. 483 func attemptCreateService(azure *gwacl.ManagementAPI, prefix, affinityGroupName, label string) (*gwacl.CreateHostedService, error) { 484 var err error 485 name := gwacl.MakeRandomHostedServiceName(prefix) 486 err = azure.CheckHostedServiceNameAvailability(name) 487 if err != nil { 488 // The calling function should retry. 489 return nil, nil 490 } 491 if label == "" { 492 label = name 493 } 494 req := gwacl.NewCreateHostedServiceWithLocation(name, label, "") 495 req.AffinityGroup = affinityGroupName 496 err = azure.AddHostedService(req) 497 if err != nil { 498 return nil, err 499 } 500 return req, nil 501 } 502 503 // newHostedService creates a hosted service. It will make up a unique name, 504 // starting with the given prefix. 505 func newHostedService(azure *gwacl.ManagementAPI, prefix, affinityGroupName, label string) (*gwacl.HostedService, error) { 506 var err error 507 var createdService *gwacl.CreateHostedService 508 for tries := 10; tries > 0 && err == nil && createdService == nil; tries-- { 509 createdService, err = attemptCreateService(azure, prefix, affinityGroupName, label) 510 } 511 if err != nil { 512 return nil, errors.Annotate(err, "could not create hosted service") 513 } 514 if createdService == nil { 515 return nil, fmt.Errorf("could not come up with a unique hosted service name - is your randomizer initialized?") 516 } 517 return azure.GetHostedServiceProperties(createdService.ServiceName, true) 518 } 519 520 // SupportedArchitectures is specified on the EnvironCapability interface. 521 func (env *azureEnviron) SupportedArchitectures() ([]string, error) { 522 env.archMutex.Lock() 523 defer env.archMutex.Unlock() 524 if env.supportedArchitectures != nil { 525 return env.supportedArchitectures, nil 526 } 527 // Create a filter to get all images from our region and for the correct stream. 528 ecfg := env.getSnapshot().ecfg 529 region := ecfg.location() 530 cloudSpec := simplestreams.CloudSpec{ 531 Region: region, 532 Endpoint: getEndpoint(region), 533 } 534 imageConstraint := imagemetadata.NewImageConstraint(simplestreams.LookupParams{ 535 CloudSpec: cloudSpec, 536 Stream: ecfg.ImageStream(), 537 }) 538 var err error 539 env.supportedArchitectures, err = common.SupportedArchitectures(env, imageConstraint) 540 return env.supportedArchitectures, err 541 } 542 543 // selectInstanceTypeAndImage returns the appropriate instances.InstanceType and 544 // the OS image name for launching a virtual machine with the given parameters. 545 func (env *azureEnviron) selectInstanceTypeAndImage(constraint *instances.InstanceConstraint) (*instances.InstanceType, string, error) { 546 ecfg := env.getSnapshot().ecfg 547 sourceImageName := ecfg.forceImageName() 548 if sourceImageName != "" { 549 // Configuration forces us to use a specific image. There may 550 // not be a suitable image in the simplestreams database. 551 // This means we can't use Juju's normal selection mechanism, 552 // because it combines instance-type and image selection: if 553 // there are no images we can use, it won't offer us an 554 // instance type either. 555 // 556 // Select the instance type using simple, Azure-specific code. 557 instanceType, err := selectMachineType(env, defaultToBaselineSpec(constraint.Constraints)) 558 if err != nil { 559 return nil, "", err 560 } 561 return instanceType, sourceImageName, nil 562 } 563 564 // Choose the most suitable instance type and OS image, based on simplestreams information. 565 spec, err := findInstanceSpec(env, constraint) 566 if err != nil { 567 return nil, "", err 568 } 569 return &spec.InstanceType, spec.Image.Id, nil 570 } 571 572 var unsupportedConstraints = []string{ 573 constraints.CpuPower, 574 constraints.Tags, 575 } 576 577 // ConstraintsValidator is defined on the Environs interface. 578 func (env *azureEnviron) ConstraintsValidator() (constraints.Validator, error) { 579 validator := constraints.NewValidator() 580 validator.RegisterUnsupported(unsupportedConstraints) 581 supportedArches, err := env.SupportedArchitectures() 582 if err != nil { 583 return nil, err 584 } 585 validator.RegisterVocabulary(constraints.Arch, supportedArches) 586 587 instanceTypes, err := listInstanceTypes(env) 588 if err != nil { 589 return nil, err 590 } 591 instTypeNames := make([]string, len(instanceTypes)) 592 for i, instanceType := range instanceTypes { 593 instTypeNames[i] = instanceType.Name 594 } 595 validator.RegisterVocabulary(constraints.InstanceType, instTypeNames) 596 validator.RegisterConflicts( 597 []string{constraints.InstanceType}, 598 []string{constraints.Mem, constraints.CpuCores, constraints.Arch, constraints.RootDisk}) 599 600 return validator, nil 601 } 602 603 // PrecheckInstance is defined on the state.Prechecker interface. 604 func (env *azureEnviron) PrecheckInstance(series string, cons constraints.Value, placement string) error { 605 if placement != "" { 606 return fmt.Errorf("unknown placement directive: %s", placement) 607 } 608 if !cons.HasInstanceType() { 609 return nil 610 } 611 // Constraint has an instance-type constraint so let's see if it is valid. 612 instanceTypes, err := listInstanceTypes(env) 613 if err != nil { 614 return err 615 } 616 for _, instanceType := range instanceTypes { 617 if instanceType.Name == *cons.InstanceType { 618 return nil 619 } 620 } 621 return fmt.Errorf("invalid instance type %q", *cons.InstanceType) 622 } 623 624 // createInstance creates all of the Azure entities necessary for a 625 // new instance. This includes Cloud Service, Deployment and Role. 626 // 627 // If serviceName is non-empty, then createInstance will assign to 628 // the Cloud Service with that name. Otherwise, a new Cloud Service 629 // will be created. 630 func (env *azureEnviron) createInstance(azure *gwacl.ManagementAPI, role *gwacl.Role, serviceName string, stateServer bool) (resultInst instance.Instance, resultErr error) { 631 var inst instance.Instance 632 defer func() { 633 if inst != nil && resultErr != nil { 634 if err := env.StopInstances(inst.Id()); err != nil { 635 // Failure upon failure. Log it, but return the original error. 636 logger.Errorf("error releasing failed instance: %v", err) 637 } 638 } 639 }() 640 var err error 641 var service *gwacl.HostedService 642 if serviceName != "" { 643 logger.Debugf("creating instance in existing cloud service %q", serviceName) 644 service, err = azure.GetHostedServiceProperties(serviceName, true) 645 } else { 646 logger.Debugf("creating instance in new cloud service") 647 // If we're creating a cloud service for state servers, 648 // we will want to open additional ports. We need to 649 // record this against the cloud service, so we use a 650 // special label for the purpose. 651 var label string 652 if stateServer { 653 label = stateServerLabel 654 } 655 service, err = newHostedService(azure, env.getEnvPrefix(), env.getAffinityGroupName(), label) 656 } 657 if err != nil { 658 return nil, err 659 } 660 if len(service.Deployments) == 0 { 661 // This is a newly created cloud service, so we 662 // should destroy it if anything below fails. 663 defer func() { 664 if resultErr != nil { 665 azure.DeleteHostedService(service.ServiceName) 666 // Destroying the hosted service destroys the instance, 667 // so ensure StopInstances isn't called. 668 inst = nil 669 } 670 }() 671 // Create an initial deployment. 672 deployment := gwacl.NewDeploymentForCreateVMDeployment( 673 deploymentNameV2(service.ServiceName), 674 deploymentSlot, 675 deploymentNameV2(service.ServiceName), 676 []gwacl.Role{*role}, 677 env.getVirtualNetworkName(), 678 ) 679 if err := azure.AddDeployment(deployment, service.ServiceName); err != nil { 680 return nil, errors.Annotate(err, "error creating VM deployment") 681 } 682 service.Deployments = append(service.Deployments, *deployment) 683 } else { 684 // Update the deployment. 685 deployment := &service.Deployments[0] 686 if err := azure.AddRole(&gwacl.AddRoleRequest{ 687 ServiceName: service.ServiceName, 688 DeploymentName: deployment.Name, 689 PersistentVMRole: (*gwacl.PersistentVMRole)(role), 690 }); err != nil { 691 return nil, err 692 } 693 deployment.RoleList = append(deployment.RoleList, *role) 694 } 695 return env.getInstance(service, role.RoleName) 696 } 697 698 // deploymentNameV1 returns the deployment name used 699 // in the original implementation of the Azure provider. 700 func deploymentNameV1(serviceName string) string { 701 return serviceName 702 } 703 704 // deploymentNameV2 returns the deployment name used 705 // in the current implementation of the Azure provider. 706 func deploymentNameV2(serviceName string) string { 707 return serviceName + "-v2" 708 } 709 710 // MaintainInstance is specified in the InstanceBroker interface. 711 func (*azureEnviron) MaintainInstance(args environs.StartInstanceParams) error { 712 return nil 713 } 714 715 // StartInstance is specified in the InstanceBroker interface. 716 func (env *azureEnviron) StartInstance(args environs.StartInstanceParams) (*environs.StartInstanceResult, error) { 717 if args.InstanceConfig.HasNetworks() { 718 return nil, errors.New("starting instances with networks is not supported yet") 719 } 720 721 err := instancecfg.FinishInstanceConfig(args.InstanceConfig, env.Config()) 722 if err != nil { 723 return nil, err 724 } 725 726 // Pick envtools. Needed for the custom data (which is what we normally 727 // call userdata). 728 args.InstanceConfig.Tools = args.Tools[0] 729 logger.Infof("picked tools %q", args.InstanceConfig.Tools) 730 731 // Compose userdata. 732 userData, err := providerinit.ComposeUserData(args.InstanceConfig, nil, AzureRenderer{}) 733 if err != nil { 734 return nil, errors.Annotate(err, "cannot compose user data") 735 } 736 737 snapshot := env.getSnapshot() 738 location := snapshot.ecfg.location() 739 instanceType, sourceImageName, err := env.selectInstanceTypeAndImage(&instances.InstanceConstraint{ 740 Region: location, 741 Series: args.Tools.OneSeries(), 742 Arches: args.Tools.Arches(), 743 Constraints: args.Constraints, 744 }) 745 if err != nil { 746 return nil, err 747 } 748 749 // We use the cloud service label as a way to group instances with 750 // the same affinity, so that machines can be be allocated to the 751 // same availability set. 752 var cloudServiceName string 753 if args.DistributionGroup != nil && snapshot.ecfg.availabilitySetsEnabled() { 754 instanceIds, err := args.DistributionGroup() 755 if err != nil { 756 return nil, err 757 } 758 for _, id := range instanceIds { 759 cloudServiceName, _ = env.splitInstanceId(id) 760 if cloudServiceName != "" { 761 break 762 } 763 } 764 } 765 766 vhd, err := env.newOSDisk(sourceImageName, args.InstanceConfig.Series) 767 if err != nil { 768 return nil, errors.Trace(err) 769 } 770 // If we're creating machine-0, we'll want to expose port 22. 771 // All other machines get an auto-generated public port for SSH. 772 stateServer := multiwatcher.AnyJobNeedsState(args.InstanceConfig.Jobs...) 773 role, err := env.newRole(instanceType.Id, vhd, stateServer, string(userData), args.InstanceConfig.Series, snapshot) 774 if err != nil { 775 return nil, errors.Trace(err) 776 } 777 inst, err := createInstance(env, snapshot.api, role, cloudServiceName, stateServer) 778 if err != nil { 779 return nil, errors.Trace(err) 780 } 781 hc := &instance.HardwareCharacteristics{ 782 Mem: &instanceType.Mem, 783 RootDisk: &instanceType.RootDisk, 784 CpuCores: &instanceType.CpuCores, 785 } 786 if len(instanceType.Arches) == 1 { 787 hc.Arch = &instanceType.Arches[0] 788 } 789 return &environs.StartInstanceResult{ 790 Instance: inst, 791 Hardware: hc, 792 }, nil 793 } 794 795 // getInstance returns an up-to-date version of the instance with the given 796 // name. 797 func (env *azureEnviron) getInstance(hostedService *gwacl.HostedService, roleName string) (instance.Instance, error) { 798 if n := len(hostedService.Deployments); n != 1 { 799 return nil, fmt.Errorf("expected one deployment for %q, got %d", hostedService.ServiceName, n) 800 } 801 deployment := &hostedService.Deployments[0] 802 803 var maskStateServerPorts bool 804 var instanceId instance.Id 805 switch deployment.Name { 806 case deploymentNameV1(hostedService.ServiceName): 807 // Old style instance. 808 instanceId = instance.Id(hostedService.ServiceName) 809 if n := len(deployment.RoleList); n != 1 { 810 return nil, fmt.Errorf("expected one role for %q, got %d", deployment.Name, n) 811 } 812 roleName = deployment.RoleList[0].RoleName 813 // In the old implementation of the Azure provider, 814 // all machines opened the state and API server ports. 815 maskStateServerPorts = true 816 817 case deploymentNameV2(hostedService.ServiceName): 818 instanceId = instance.Id(fmt.Sprintf("%s-%s", hostedService.ServiceName, roleName)) 819 // Newly created state server machines are put into 820 // the cloud service with the stateServerLabel label. 821 if decoded, err := base64.StdEncoding.DecodeString(hostedService.Label); err == nil { 822 maskStateServerPorts = string(decoded) == stateServerLabel 823 } 824 } 825 826 var roleInstance *gwacl.RoleInstance 827 for _, role := range deployment.RoleInstanceList { 828 if role.RoleName == roleName { 829 roleInstance = &role 830 break 831 } 832 } 833 834 instance := &azureInstance{ 835 environ: env, 836 hostedService: &hostedService.HostedServiceDescriptor, 837 instanceId: instanceId, 838 deploymentName: deployment.Name, 839 roleName: roleName, 840 roleInstance: roleInstance, 841 maskStateServerPorts: maskStateServerPorts, 842 } 843 return instance, nil 844 } 845 846 // newOSDisk creates a gwacl.OSVirtualHardDisk object suitable for an 847 // Azure Virtual Machine. 848 func (env *azureEnviron) newOSDisk(sourceImageName string, ser string) (*gwacl.OSVirtualHardDisk, error) { 849 vhdName := gwacl.MakeRandomDiskName("juju") 850 vhdPath := fmt.Sprintf("vhds/%s", vhdName) 851 snap := env.getSnapshot() 852 storageAccount := snap.ecfg.storageAccountName() 853 mediaLink := gwacl.CreateVirtualHardDiskMediaLink(storageAccount, vhdPath) 854 os, err := series.GetOSFromSeries(ser) 855 if err != nil { 856 return nil, errors.Trace(err) 857 } 858 var OSType string 859 switch os { 860 case jujuos.Windows: 861 OSType = "Windows" 862 default: 863 OSType = "Linux" 864 } 865 // The disk label is optional and the disk name can be omitted if 866 // mediaLink is provided. 867 return gwacl.NewOSVirtualHardDisk("", "", "", mediaLink, sourceImageName, OSType), nil 868 } 869 870 // getInitialEndpoints returns a slice of the endpoints every instance should have open 871 // (ssh port, etc). 872 func (env *azureEnviron) getInitialEndpoints(stateServer bool) []gwacl.InputEndpoint { 873 cfg := env.Config() 874 endpoints := []gwacl.InputEndpoint{{ 875 LocalPort: 22, 876 Name: "sshport", 877 Port: 22, 878 Protocol: "tcp", 879 }} 880 if stateServer { 881 endpoints = append(endpoints, []gwacl.InputEndpoint{{ 882 LocalPort: cfg.APIPort(), 883 Port: cfg.APIPort(), 884 Protocol: "tcp", 885 Name: "apiport", 886 }}...) 887 } 888 for i, endpoint := range endpoints { 889 endpoint.LoadBalancedEndpointSetName = endpoint.Name 890 endpoint.LoadBalancerProbe = &gwacl.LoadBalancerProbe{ 891 Port: endpoint.Port, 892 Protocol: "TCP", 893 } 894 endpoints[i] = endpoint 895 } 896 return endpoints 897 } 898 899 // newRole creates a gwacl.Role object (an Azure Virtual Machine) which uses 900 // the given Virtual Hard Drive. 901 // 902 // roleSize is the name of one of Azure's machine types, e.g. ExtraSmall, 903 // Large, A6 etc. 904 func (env *azureEnviron) newRole(roleSize string, vhd *gwacl.OSVirtualHardDisk, stateServer bool, userdata, ser string, snapshot *azureEnviron) (*gwacl.Role, error) { 905 // Do some common initialization 906 roleName := gwacl.MakeRandomRoleName("juju") 907 hostname := roleName 908 password := gwacl.MakeRandomPassword() 909 910 os, err := series.GetOSFromSeries(ser) 911 if err != nil { 912 return nil, errors.Trace(err) 913 } 914 // Generate a Network Configuration with the initially required ports open. 915 networkConfigurationSet := gwacl.NewNetworkConfigurationSet(env.getInitialEndpoints(stateServer), nil) 916 917 var role *gwacl.Role 918 switch os { 919 case jujuos.Windows: 920 role, err = makeWindowsRole(password, roleSize, roleName, userdata, vhd, networkConfigurationSet, snapshot) 921 default: 922 role, err = makeLinuxRole(hostname, password, roleSize, roleName, userdata, vhd, networkConfigurationSet) 923 } 924 if err != nil { 925 return nil, errors.Trace(err) 926 } 927 role.AvailabilitySetName = "juju" 928 return role, nil 929 } 930 931 // makeLinuxRole will create a gwacl.Role for a Linux VM. 932 // The VM will have: 933 // - an 'ubuntu' user defined with an unguessable (randomly generated) password 934 // - its ssh port (TCP 22) open 935 // (if a state server) 936 // - its state port (TCP mongoDB) port open 937 // - its API port (TCP) open 938 // On Linux the userdata is sent as a base64 encoded string in the CustomData xml field of the role. 939 func makeLinuxRole( 940 hostname, password, roleSize, roleName, userdata string, 941 vhd *gwacl.OSVirtualHardDisk, networkConfigSet *gwacl.ConfigurationSet) (*gwacl.Role, error) { 942 // Create a Linux Configuration with the username and the password 943 // empty and disable SSH with password authentication. 944 username := "ubuntu" 945 cfgSet := gwacl.NewLinuxProvisioningConfigurationSet(hostname, username, password, userdata, "true") 946 finalCfgSet := []gwacl.ConfigurationSet{*cfgSet, *networkConfigSet} 947 return gwacl.NewLinuxRole(roleSize, roleName, vhd, finalCfgSet), nil 948 949 } 950 951 // makeWindowsRole will create a gwacl.Role for a Windows VM. 952 // The VM will have: 953 // - an 'JujuAdministrator' user defined with an unguessable (randomly generated) password 954 // TODO(bogdanteleaga): Open the winrm port and provide winrm access 955 // - for now only port 22 is open(by default) 956 // On Windows the userdata is uploaded to the storage and then a reference to it is sent 957 // using CustomScriptExtension. For more details see makeUserdataResourceExtension 958 func makeWindowsRole( 959 password, roleSize, roleName, userdata string, vhd *gwacl.OSVirtualHardDisk, 960 networkConfigSet *gwacl.ConfigurationSet, snapshot *azureEnviron) (*gwacl.Role, error) { 961 // Later we will add WinRM configuration here 962 // Windows does not accept hostnames over 15 characters. 963 roleName = roleName[:15] 964 hostname := roleName 965 username := "JujuAdministrator" 966 cfgSet := gwacl.NewWindowsProvisioningConfigurationSet(hostname, password, "true", "", nil, nil, username, "", userdata) 967 finalCfgSet := []gwacl.ConfigurationSet{*cfgSet, *networkConfigSet} 968 resourceExtension, err := makeUserdataResourceExtension(hostname, userdata, snapshot) 969 if err != nil { 970 return nil, errors.Trace(err) 971 } 972 return gwacl.NewWindowsRole(roleSize, roleName, vhd, finalCfgSet, &[]gwacl.ResourceExtensionReference{*resourceExtension}, "true"), nil 973 974 } 975 976 // makeUserdataResourceExtension will upload the userdata to storage and then fill in the proper xml 977 // following the example here 978 // https://msdn.microsoft.com/en-us/library/azure/dn781373.aspx 979 func makeUserdataResourceExtension(nonce string, userData string, snapshot *azureEnviron) (*gwacl.ResourceExtensionReference, error) { 980 // The bootstrap userdata script is the same for all machines. 981 // So we first check if it's already uploaded and if it isn't we upload it 982 _, err := snapshot.storage.Get(bootstrapUserdataScriptFilename) 983 if errors.IsNotFound(err) { 984 err := snapshot.storage.Put(bootstrapUserdataScriptFilename, bytes.NewReader([]byte(bootstrapUserdataScript)), int64(len(bootstrapUserdataScript))) 985 if err != nil { 986 logger.Errorf(err.Error()) 987 return nil, errors.Annotate(err, "cannot upload userdata to storage") 988 } 989 } 990 991 uri, err := snapshot.storage.URL(bootstrapUserdataScriptFilename) 992 if err != nil { 993 logger.Errorf(err.Error()) 994 return nil, errors.Trace(err) 995 } 996 997 scriptPublicConfig, err := makeUserdataResourceScripts(uri, bootstrapUserdataScriptFilename) 998 if err != nil { 999 return nil, errors.Trace(err) 1000 } 1001 publicParam := gwacl.NewResourceExtensionParameter("CustomScriptExtensionPublicConfigParameter", scriptPublicConfig, gwacl.ResourceExtensionParameterTypePublic) 1002 return gwacl.NewResourceExtensionReference("MyCustomScriptExtension", "Microsoft.Compute", "CustomScriptExtension", "1.4", "", []gwacl.ResourceExtensionParameter{*publicParam}), nil 1003 } 1004 1005 func makeUserdataResourceScripts(uri, filename string) (publicParam string, err error) { 1006 type publicConfig struct { 1007 FileUris []string `json:"fileUris"` 1008 CommandToExecute string `json:"commandToExecute"` 1009 } 1010 1011 public := publicConfig{ 1012 FileUris: []string{uri}, 1013 CommandToExecute: fmt.Sprintf("powershell -ExecutionPolicy Unrestricted -file %s", filename), 1014 } 1015 1016 publicConf, err := json.Marshal(public) 1017 if err != nil { 1018 return "", errors.Trace(err) 1019 } 1020 1021 scriptPublicConfig := base64.StdEncoding.EncodeToString(publicConf) 1022 1023 return scriptPublicConfig, nil 1024 } 1025 1026 // StopInstances is specified in the InstanceBroker interface. 1027 func (env *azureEnviron) StopInstances(ids ...instance.Id) error { 1028 snap := env.getSnapshot() 1029 1030 // Map services to role names we want to delete. 1031 serviceInstances := make(map[string]map[string]bool) 1032 var serviceNames []string 1033 for _, id := range ids { 1034 serviceName, roleName := env.splitInstanceId(id) 1035 if roleName == "" { 1036 serviceInstances[serviceName] = nil 1037 serviceNames = append(serviceNames, serviceName) 1038 } else { 1039 deleteRoleNames, ok := serviceInstances[serviceName] 1040 if !ok { 1041 deleteRoleNames = make(map[string]bool) 1042 serviceInstances[serviceName] = deleteRoleNames 1043 serviceNames = append(serviceNames, serviceName) 1044 } 1045 deleteRoleNames[roleName] = true 1046 } 1047 } 1048 1049 // Load the properties of each service, so we know whether to 1050 // delete the entire service. 1051 // 1052 // Note: concurrent operations on Affinity Groups have been 1053 // found to cause conflict responses, so we do everything serially. 1054 for _, serviceName := range serviceNames { 1055 deleteRoleNames := serviceInstances[serviceName] 1056 service, err := snap.api.GetHostedServiceProperties(serviceName, true) 1057 if err != nil { 1058 return err 1059 } else if len(service.Deployments) != 1 { 1060 continue 1061 } 1062 // Filter the instances that have no corresponding role. 1063 roleNames := make(set.Strings) 1064 for _, role := range service.Deployments[0].RoleList { 1065 roleNames.Add(role.RoleName) 1066 } 1067 for roleName := range deleteRoleNames { 1068 if !roleNames.Contains(roleName) { 1069 delete(deleteRoleNames, roleName) 1070 } 1071 } 1072 // If we're deleting all the roles, we need to delete the 1073 // entire cloud service or we'll get an error. deleteRoleNames 1074 // is nil if we're dealing with a legacy deployment. 1075 if deleteRoleNames == nil || len(deleteRoleNames) == roleNames.Size() { 1076 if err := snap.api.DeleteHostedService(serviceName); err != nil { 1077 return err 1078 } 1079 } else { 1080 for roleName := range deleteRoleNames { 1081 if err := snap.api.DeleteRole(&gwacl.DeleteRoleRequest{ 1082 ServiceName: serviceName, 1083 DeploymentName: service.Deployments[0].Name, 1084 RoleName: roleName, 1085 DeleteMedia: true, 1086 }); err != nil { 1087 return err 1088 } 1089 } 1090 } 1091 } 1092 return nil 1093 } 1094 1095 // hostedServices returns all services for this environment. 1096 func (env *azureEnviron) hostedServices() ([]gwacl.HostedServiceDescriptor, error) { 1097 snap := env.getSnapshot() 1098 services, err := snap.api.ListHostedServices() 1099 if err != nil { 1100 return nil, err 1101 } 1102 1103 var filteredServices []gwacl.HostedServiceDescriptor 1104 // Service names are prefixed with the environment name, followed by "-". 1105 // We must be careful not to include services where the environment name 1106 // is a substring of another name. ie we mustn't allow "azure" to match "azure-1". 1107 envPrefix := env.getEnvPrefix() 1108 // Just in case. 1109 filterPrefix := regexp.QuoteMeta(envPrefix) 1110 1111 // Now filter the services. 1112 prefixMatch := regexp.MustCompile("^" + filterPrefix + "[^-]*$") 1113 for _, service := range services { 1114 if prefixMatch.Match([]byte(service.ServiceName)) { 1115 filteredServices = append(filteredServices, service) 1116 } 1117 } 1118 return filteredServices, nil 1119 } 1120 1121 // destroyAllServices destroys all Cloud Services and deployments contained. 1122 // This is needed to clean up broken environments, in which there are cloud 1123 // services with no deployments. 1124 func (env *azureEnviron) destroyAllServices() error { 1125 services, err := env.hostedServices() 1126 if err != nil { 1127 return err 1128 } 1129 snap := env.getSnapshot() 1130 for _, service := range services { 1131 if err := snap.api.DeleteHostedService(service.ServiceName); err != nil { 1132 return err 1133 } 1134 } 1135 return nil 1136 } 1137 1138 // splitInstanceId splits the specified instance.Id into its 1139 // cloud-service and role parts. Both values will be empty 1140 // if the instance-id is non-matching, and role will be empty 1141 // for legacy instance-ids. 1142 func (env *azureEnviron) splitInstanceId(id instance.Id) (service, role string) { 1143 prefix := env.getEnvPrefix() 1144 if !strings.HasPrefix(string(id), prefix) { 1145 return "", "" 1146 } 1147 fields := strings.Split(string(id)[len(prefix):], "-") 1148 service = prefix + fields[0] 1149 if len(fields) > 1 { 1150 role = fields[1] 1151 } 1152 return service, role 1153 } 1154 1155 // Instances is specified in the Environ interface. 1156 func (env *azureEnviron) Instances(ids []instance.Id) ([]instance.Instance, error) { 1157 snap := env.getSnapshot() 1158 1159 type instanceId struct { 1160 serviceName, roleName string 1161 } 1162 1163 instancesIds := make([]instanceId, len(ids)) 1164 serviceNames := make(set.Strings) 1165 for i, id := range ids { 1166 serviceName, roleName := env.splitInstanceId(id) 1167 if serviceName == "" { 1168 continue 1169 } 1170 instancesIds[i] = instanceId{ 1171 serviceName: serviceName, 1172 roleName: roleName, 1173 } 1174 serviceNames.Add(serviceName) 1175 } 1176 1177 // Map service names to gwacl.HostedServices. 1178 services, err := snap.api.ListSpecificHostedServices(&gwacl.ListSpecificHostedServicesRequest{ 1179 ServiceNames: serviceNames.Values(), 1180 }) 1181 if err != nil { 1182 return nil, err 1183 } 1184 if len(services) == 0 { 1185 return nil, environs.ErrNoInstances 1186 } 1187 hostedServices := make(map[string]*gwacl.HostedService) 1188 for _, s := range services { 1189 hostedService, err := snap.api.GetHostedServiceProperties(s.ServiceName, true) 1190 if err != nil { 1191 return nil, err 1192 } 1193 hostedServices[s.ServiceName] = hostedService 1194 } 1195 1196 var validInstances int 1197 instances := make([]instance.Instance, len(ids)) 1198 for i, id := range instancesIds { 1199 if id.serviceName == "" { 1200 // Previously determined to be an invalid instance ID. 1201 continue 1202 } 1203 hostedService := hostedServices[id.serviceName] 1204 instance, err := snap.getInstance(hostedService, id.roleName) 1205 if err == nil { 1206 instances[i] = instance 1207 validInstances++ 1208 } else { 1209 logger.Debugf("failed to get instance for role %q in service %q: %v", id.roleName, hostedService.ServiceName, err) 1210 } 1211 } 1212 1213 switch validInstances { 1214 case len(instances): 1215 err = nil 1216 case 0: 1217 instances = nil 1218 err = environs.ErrNoInstances 1219 default: 1220 err = environs.ErrPartialInstances 1221 } 1222 return instances, err 1223 } 1224 1225 // AllInstances is specified in the InstanceBroker interface. 1226 func (env *azureEnviron) AllInstances() ([]instance.Instance, error) { 1227 // The instance list is built using the list of all the Azure 1228 // Services (instance==service). 1229 // Acquire management API object. 1230 snap := env.getSnapshot() 1231 1232 serviceDescriptors, err := env.hostedServices() 1233 if err != nil { 1234 return nil, err 1235 } 1236 1237 var instances []instance.Instance 1238 for _, sd := range serviceDescriptors { 1239 hostedService, err := snap.api.GetHostedServiceProperties(sd.ServiceName, true) 1240 if err != nil { 1241 return nil, err 1242 } else if len(hostedService.Deployments) != 1 { 1243 continue 1244 } 1245 deployment := &hostedService.Deployments[0] 1246 for _, role := range deployment.RoleList { 1247 instance, err := snap.getInstance(hostedService, role.RoleName) 1248 if err != nil { 1249 return nil, err 1250 } 1251 instances = append(instances, instance) 1252 } 1253 } 1254 return instances, nil 1255 } 1256 1257 // getEnvPrefix returns the prefix used to name the objects specific to this 1258 // environment. The environment prefix name is immutable, so there is no need 1259 // to use a configuration snapshot. 1260 func (env *azureEnviron) getEnvPrefix() string { 1261 return fmt.Sprintf("juju-%s-", env.Config().Name()) 1262 } 1263 1264 // Storage is specified in the Environ interface. 1265 func (env *azureEnviron) Storage() storage.Storage { 1266 return env.getSnapshot().storage 1267 } 1268 1269 // Destroy is specified in the Environ interface. 1270 func (env *azureEnviron) Destroy() error { 1271 logger.Debugf("destroying environment %q", env.Config().Name()) 1272 1273 // Stop all instances. 1274 if err := env.destroyAllServices(); err != nil { 1275 return fmt.Errorf("cannot destroy instances: %v", err) 1276 } 1277 1278 // Delete vnet and affinity group. Deleting the virtual network 1279 // may fail for inexplicable reasons (cannot delete in the Azure 1280 // console either for some amount of time after deleting dependent 1281 // VMs), so we only treat this as a warning. There is no cost 1282 // associated with a vnet or affinity group. 1283 if err := env.deleteVirtualNetwork(); err != nil { 1284 logger.Warningf("cannot delete the environment's virtual network: %v", err) 1285 } 1286 if err := env.deleteAffinityGroup(); err != nil { 1287 logger.Warningf("cannot delete the environment's affinity group: %v", err) 1288 } 1289 1290 // Delete storage. 1291 // Deleting the storage is done last so that if something fails 1292 // half way through the Destroy() method, the storage won't be cleaned 1293 // up and thus an attempt to re-boostrap the environment will lead to 1294 // a "error: environment is already bootstrapped" error. 1295 if err := env.Storage().RemoveAll(); err != nil { 1296 return fmt.Errorf("cannot clean up storage: %v", err) 1297 } 1298 return nil 1299 } 1300 1301 // OpenPorts is specified in the Environ interface. However, Azure does not 1302 // support the global firewall mode. 1303 func (env *azureEnviron) OpenPorts(ports []network.PortRange) error { 1304 return nil 1305 } 1306 1307 // ClosePorts is specified in the Environ interface. However, Azure does not 1308 // support the global firewall mode. 1309 func (env *azureEnviron) ClosePorts(ports []network.PortRange) error { 1310 return nil 1311 } 1312 1313 // Ports is specified in the Environ interface. 1314 func (env *azureEnviron) Ports() ([]network.PortRange, error) { 1315 // TODO: implement this. 1316 return []network.PortRange{}, nil 1317 } 1318 1319 // Provider is specified in the Environ interface. 1320 func (env *azureEnviron) Provider() environs.EnvironProvider { 1321 return azureEnvironProvider{} 1322 } 1323 1324 var ( 1325 retryPolicy = gwacl.RetryPolicy{ 1326 NbRetries: 6, 1327 HttpStatusCodes: []int{ 1328 http.StatusConflict, 1329 http.StatusRequestTimeout, 1330 http.StatusInternalServerError, 1331 http.StatusServiceUnavailable, 1332 }, 1333 Delay: 10 * time.Second} 1334 ) 1335 1336 // updateStorageAccountKey queries the storage account key, and updates the 1337 // version cached in env.storageAccountKey. 1338 // 1339 // It takes a snapshot in order to preserve transactional integrity relative 1340 // to the snapshot's starting state, without having to lock the environment 1341 // for the duration. If there is a conflicting change to env relative to the 1342 // state recorded in the snapshot, this function will fail. 1343 func (env *azureEnviron) updateStorageAccountKey(snapshot *azureEnviron) (string, error) { 1344 // This method follows an RCU pattern, an optimistic technique to 1345 // implement atomic read-update transactions: get a consistent snapshot 1346 // of state; process data; enter critical section; check for conflicts; 1347 // write back changes. The advantage is that there are no long-held 1348 // locks, in particular while waiting for the request to Azure to 1349 // complete. 1350 // "Get a consistent snapshot of state" is the caller's responsibility. 1351 // The caller can use env.getSnapshot(). 1352 1353 // Process data: get a current account key from Azure. 1354 key, err := env.queryStorageAccountKey() 1355 if err != nil { 1356 return "", err 1357 } 1358 1359 // Enter critical section. 1360 env.Lock() 1361 defer env.Unlock() 1362 1363 // Check for conflicts: is the config still what it was? 1364 if env.ecfg != snapshot.ecfg { 1365 // The environment has been reconfigured while we were 1366 // working on this, so the key we just get may not be 1367 // appropriate any longer. So fail. 1368 // Whatever we were doing isn't likely to be right any more 1369 // anyway. Otherwise, it might be worth returning the key 1370 // just in case it still works, and proceed without updating 1371 // env.storageAccountKey. 1372 return "", fmt.Errorf("environment was reconfigured") 1373 } 1374 1375 // Write back changes. 1376 env.storageAccountKey = key 1377 return key, nil 1378 } 1379 1380 // getStorageContext obtains a context object for interfacing with Azure's 1381 // storage API. 1382 // For now, each invocation just returns a separate object. This is probably 1383 // wasteful (each context gets its own SSL connection) and may need optimizing 1384 // later. 1385 func (env *azureEnviron) getStorageContext() (*gwacl.StorageContext, error) { 1386 snap := env.getSnapshot() 1387 key := snap.storageAccountKey 1388 if key == "" { 1389 // We don't know the storage-account key yet. Request it. 1390 var err error 1391 key, err = env.updateStorageAccountKey(snap) 1392 if err != nil { 1393 return nil, err 1394 } 1395 } 1396 context := gwacl.StorageContext{ 1397 Account: snap.ecfg.storageAccountName(), 1398 Key: key, 1399 AzureEndpoint: gwacl.GetEndpoint(snap.ecfg.location()), 1400 RetryPolicy: retryPolicy, 1401 } 1402 return &context, nil 1403 } 1404 1405 // TODO(ericsnow) lp-1398055 1406 // Implement the ZonedEnviron interface. 1407 1408 // Region is specified in the HasRegion interface. 1409 func (env *azureEnviron) Region() (simplestreams.CloudSpec, error) { 1410 ecfg := env.getSnapshot().ecfg 1411 return simplestreams.CloudSpec{ 1412 Region: ecfg.location(), 1413 Endpoint: string(gwacl.GetEndpoint(ecfg.location())), 1414 }, nil 1415 } 1416 1417 // SupportsUnitPlacement is specified in the state.EnvironCapability interface. 1418 func (env *azureEnviron) SupportsUnitPlacement() error { 1419 if env.getSnapshot().ecfg.availabilitySetsEnabled() { 1420 return fmt.Errorf("unit placement is not supported with availability-sets-enabled") 1421 } 1422 return nil 1423 }