github.com/cloudbase/juju-core@v0.0.0-20140504232958-a7271ac7912f/provider/ec2/ec2.go (about) 1 // Copyright 2011, 2012, 2013 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 4 package ec2 5 6 import ( 7 "fmt" 8 "io/ioutil" 9 "net/http" 10 "strings" 11 "sync" 12 "time" 13 14 "github.com/juju/loggo" 15 "launchpad.net/goamz/aws" 16 "launchpad.net/goamz/ec2" 17 "launchpad.net/goamz/s3" 18 19 "launchpad.net/juju-core/constraints" 20 "launchpad.net/juju-core/environs" 21 "launchpad.net/juju-core/environs/cloudinit" 22 "launchpad.net/juju-core/environs/config" 23 "launchpad.net/juju-core/environs/imagemetadata" 24 "launchpad.net/juju-core/environs/instances" 25 "launchpad.net/juju-core/environs/simplestreams" 26 "launchpad.net/juju-core/environs/storage" 27 envtools "launchpad.net/juju-core/environs/tools" 28 "launchpad.net/juju-core/instance" 29 "launchpad.net/juju-core/provider/common" 30 "launchpad.net/juju-core/state" 31 "launchpad.net/juju-core/state/api" 32 "launchpad.net/juju-core/tools" 33 "launchpad.net/juju-core/utils" 34 ) 35 36 var logger = loggo.GetLogger("juju.provider.ec2") 37 38 // Use shortAttempt to poll for short-term events. 39 var shortAttempt = utils.AttemptStrategy{ 40 Total: 5 * time.Second, 41 Delay: 200 * time.Millisecond, 42 } 43 44 func init() { 45 environs.RegisterProvider("ec2", environProvider{}) 46 } 47 48 type environProvider struct{} 49 50 var providerInstance environProvider 51 52 type environ struct { 53 name string 54 55 // ecfgMutex protects the *Unlocked fields below. 56 ecfgMutex sync.Mutex 57 ecfgUnlocked *environConfig 58 ec2Unlocked *ec2.EC2 59 s3Unlocked *s3.S3 60 storageUnlocked storage.Storage 61 } 62 63 var _ environs.Environ = (*environ)(nil) 64 var _ simplestreams.HasRegion = (*environ)(nil) 65 var _ imagemetadata.SupportsCustomSources = (*environ)(nil) 66 var _ envtools.SupportsCustomSources = (*environ)(nil) 67 68 type ec2Instance struct { 69 e *environ 70 71 mu sync.Mutex 72 *ec2.Instance 73 } 74 75 func (inst *ec2Instance) String() string { 76 return string(inst.Id()) 77 } 78 79 var _ instance.Instance = (*ec2Instance)(nil) 80 81 func (inst *ec2Instance) getInstance() *ec2.Instance { 82 inst.mu.Lock() 83 defer inst.mu.Unlock() 84 return inst.Instance 85 } 86 87 func (inst *ec2Instance) Id() instance.Id { 88 return instance.Id(inst.getInstance().InstanceId) 89 } 90 91 func (inst *ec2Instance) Status() string { 92 return inst.getInstance().State.Name 93 } 94 95 // Refresh implements instance.Refresh(), requerying the 96 // Instance details over the ec2 api 97 func (inst *ec2Instance) Refresh() error { 98 _, err := inst.refresh() 99 return err 100 } 101 102 // refresh requeries Instance details over the ec2 api. 103 func (inst *ec2Instance) refresh() (*ec2.Instance, error) { 104 id := inst.Id() 105 insts, err := inst.e.Instances([]instance.Id{id}) 106 if err != nil { 107 return nil, err 108 } 109 inst.mu.Lock() 110 defer inst.mu.Unlock() 111 inst.Instance = insts[0].(*ec2Instance).Instance 112 return inst.Instance, nil 113 } 114 115 // Addresses implements instance.Addresses() returning generic address 116 // details for the instance, and requerying the ec2 api if required. 117 func (inst *ec2Instance) Addresses() ([]instance.Address, error) { 118 // TODO(gz): Stop relying on this requerying logic, maybe remove error 119 instInstance := inst.getInstance() 120 if instInstance.DNSName == "" { 121 // Fetch the instance information again, in case 122 // the DNS information has become available. 123 var err error 124 instInstance, err = inst.refresh() 125 if err != nil { 126 return nil, err 127 } 128 } 129 var addresses []instance.Address 130 possibleAddresses := []instance.Address{ 131 { 132 Value: instInstance.DNSName, 133 Type: instance.HostName, 134 NetworkScope: instance.NetworkPublic, 135 }, 136 { 137 Value: instInstance.PrivateDNSName, 138 Type: instance.HostName, 139 NetworkScope: instance.NetworkCloudLocal, 140 }, 141 { 142 Value: instInstance.IPAddress, 143 Type: instance.Ipv4Address, 144 NetworkScope: instance.NetworkPublic, 145 }, 146 { 147 Value: instInstance.PrivateIPAddress, 148 Type: instance.Ipv4Address, 149 NetworkScope: instance.NetworkCloudLocal, 150 }, 151 } 152 for _, address := range possibleAddresses { 153 if address.Value != "" { 154 addresses = append(addresses, address) 155 } 156 } 157 return addresses, nil 158 } 159 160 func (inst *ec2Instance) DNSName() (string, error) { 161 addresses, err := inst.Addresses() 162 if err != nil { 163 return "", err 164 } 165 addr := instance.SelectPublicAddress(addresses) 166 if addr == "" { 167 return "", instance.ErrNoDNSName 168 } 169 return addr, nil 170 171 } 172 173 func (inst *ec2Instance) WaitDNSName() (string, error) { 174 return common.WaitDNSName(inst) 175 } 176 177 func (p environProvider) BoilerplateConfig() string { 178 return ` 179 # https://juju.ubuntu.com/docs/config-aws.html 180 amazon: 181 type: ec2 182 # region specifies the ec2 region. It defaults to us-east-1. 183 # region: us-east-1 184 # 185 # access-key holds the ec2 access key. It defaults to the environment 186 # variable AWS_ACCESS_KEY_ID. 187 # access-key: <secret> 188 # 189 # secret-key holds the ec2 secret key. It defaults to the environment 190 # variable AWS_SECRET_ACCESS_KEY. 191 # 192 # image-stream chooses a simplestreams stream to select OS images from, 193 # for example daily or released images (or any other stream available on simplestreams). 194 # image-stream: "released" 195 196 `[1:] 197 } 198 199 func (p environProvider) Open(cfg *config.Config) (environs.Environ, error) { 200 logger.Infof("opening environment %q", cfg.Name()) 201 e := new(environ) 202 e.name = cfg.Name() 203 err := e.SetConfig(cfg) 204 if err != nil { 205 return nil, err 206 } 207 return e, nil 208 } 209 210 func (p environProvider) Prepare(ctx environs.BootstrapContext, cfg *config.Config) (environs.Environ, error) { 211 attrs := cfg.UnknownAttrs() 212 if _, ok := attrs["control-bucket"]; !ok { 213 uuid, err := utils.NewUUID() 214 if err != nil { 215 return nil, err 216 } 217 attrs["control-bucket"] = fmt.Sprintf("%x", uuid.Raw()) 218 } 219 cfg, err := cfg.Apply(attrs) 220 if err != nil { 221 return nil, err 222 } 223 return p.Open(cfg) 224 } 225 226 // MetadataLookupParams returns parameters which are used to query image metadata to 227 // find matching image information. 228 func (p environProvider) MetadataLookupParams(region string) (*simplestreams.MetadataLookupParams, error) { 229 if region == "" { 230 fmt.Errorf("region must be specified") 231 } 232 ec2Region, ok := allRegions[region] 233 if !ok { 234 return nil, fmt.Errorf("unknown region %q", region) 235 } 236 return &simplestreams.MetadataLookupParams{ 237 Region: region, 238 Endpoint: ec2Region.EC2Endpoint, 239 Architectures: []string{"amd64", "i386"}, 240 }, nil 241 } 242 243 func (environProvider) SecretAttrs(cfg *config.Config) (map[string]string, error) { 244 m := make(map[string]string) 245 ecfg, err := providerInstance.newConfig(cfg) 246 if err != nil { 247 return nil, err 248 } 249 m["access-key"] = ecfg.accessKey() 250 m["secret-key"] = ecfg.secretKey() 251 return m, nil 252 } 253 254 func (environProvider) PublicAddress() (string, error) { 255 return fetchMetadata("public-hostname") 256 } 257 258 func (environProvider) PrivateAddress() (string, error) { 259 return fetchMetadata("local-hostname") 260 } 261 262 func (e *environ) Config() *config.Config { 263 return e.ecfg().Config 264 } 265 266 func (e *environ) SetConfig(cfg *config.Config) error { 267 ecfg, err := providerInstance.newConfig(cfg) 268 if err != nil { 269 return err 270 } 271 e.ecfgMutex.Lock() 272 defer e.ecfgMutex.Unlock() 273 e.ecfgUnlocked = ecfg 274 275 auth := aws.Auth{ecfg.accessKey(), ecfg.secretKey()} 276 region := aws.Regions[ecfg.region()] 277 e.ec2Unlocked = ec2.New(auth, region) 278 e.s3Unlocked = s3.New(auth, region) 279 280 // create new storage instances, existing instances continue 281 // to reference their existing configuration. 282 e.storageUnlocked = &ec2storage{ 283 bucket: e.s3Unlocked.Bucket(ecfg.controlBucket()), 284 } 285 return nil 286 } 287 288 func (e *environ) ecfg() *environConfig { 289 e.ecfgMutex.Lock() 290 ecfg := e.ecfgUnlocked 291 e.ecfgMutex.Unlock() 292 return ecfg 293 } 294 295 func (e *environ) ec2() *ec2.EC2 { 296 e.ecfgMutex.Lock() 297 ec2 := e.ec2Unlocked 298 e.ecfgMutex.Unlock() 299 return ec2 300 } 301 302 func (e *environ) s3() *s3.S3 { 303 e.ecfgMutex.Lock() 304 s3 := e.s3Unlocked 305 e.ecfgMutex.Unlock() 306 return s3 307 } 308 309 func (e *environ) Name() string { 310 return e.name 311 } 312 313 func (e *environ) Storage() storage.Storage { 314 e.ecfgMutex.Lock() 315 stor := e.storageUnlocked 316 e.ecfgMutex.Unlock() 317 return stor 318 } 319 320 func (e *environ) Bootstrap(ctx environs.BootstrapContext, cons constraints.Value) error { 321 return common.Bootstrap(ctx, e, cons) 322 } 323 324 func (e *environ) StateInfo() (*state.Info, *api.Info, error) { 325 return common.StateInfo(e) 326 } 327 328 // MetadataLookupParams returns parameters which are used to query simplestreams metadata. 329 func (e *environ) MetadataLookupParams(region string) (*simplestreams.MetadataLookupParams, error) { 330 if region == "" { 331 region = e.ecfg().region() 332 } 333 ec2Region, ok := allRegions[region] 334 if !ok { 335 return nil, fmt.Errorf("unknown region %q", region) 336 } 337 return &simplestreams.MetadataLookupParams{ 338 Series: e.ecfg().DefaultSeries(), 339 Region: region, 340 Endpoint: ec2Region.EC2Endpoint, 341 Architectures: []string{"amd64", "i386", "arm", "arm64", "ppc64"}, 342 }, nil 343 } 344 345 // Region is specified in the HasRegion interface. 346 func (e *environ) Region() (simplestreams.CloudSpec, error) { 347 region := e.ecfg().region() 348 ec2Region, ok := allRegions[region] 349 if !ok { 350 return simplestreams.CloudSpec{}, fmt.Errorf("unknown region %q", region) 351 } 352 return simplestreams.CloudSpec{ 353 Region: region, 354 Endpoint: ec2Region.EC2Endpoint, 355 }, nil 356 } 357 358 const ebsStorage = "ebs" 359 360 // StartInstance is specified in the InstanceBroker interface. 361 func (e *environ) StartInstance(cons constraints.Value, possibleTools tools.List, 362 machineConfig *cloudinit.MachineConfig) (instance.Instance, *instance.HardwareCharacteristics, error) { 363 364 arches := possibleTools.Arches() 365 stor := ebsStorage 366 sources, err := imagemetadata.GetMetadataSources(e) 367 if err != nil { 368 return nil, nil, err 369 } 370 371 series := possibleTools.OneSeries() 372 spec, err := findInstanceSpec(sources, e.Config().ImageStream(), &instances.InstanceConstraint{ 373 Region: e.ecfg().region(), 374 Series: series, 375 Arches: arches, 376 Constraints: cons, 377 Storage: &stor, 378 }) 379 if err != nil { 380 return nil, nil, err 381 } 382 tools, err := possibleTools.Match(tools.Filter{Arch: spec.Image.Arch}) 383 if err != nil { 384 return nil, nil, fmt.Errorf("chosen architecture %v not present in %v", spec.Image.Arch, arches) 385 } 386 387 machineConfig.Tools = tools[0] 388 if err := environs.FinishMachineConfig(machineConfig, e.Config(), cons); err != nil { 389 return nil, nil, err 390 } 391 392 userData, err := environs.ComposeUserData(machineConfig) 393 if err != nil { 394 return nil, nil, fmt.Errorf("cannot make user data: %v", err) 395 } 396 logger.Debugf("ec2 user data; %d bytes", len(userData)) 397 cfg := e.Config() 398 groups, err := e.setUpGroups(machineConfig.MachineId, cfg.StatePort(), cfg.APIPort()) 399 if err != nil { 400 return nil, nil, fmt.Errorf("cannot set up groups: %v", err) 401 } 402 var instResp *ec2.RunInstancesResp 403 404 device, diskSize := getDiskSize(cons) 405 for a := shortAttempt.Start(); a.Next(); { 406 instResp, err = e.ec2().RunInstances(&ec2.RunInstances{ 407 ImageId: spec.Image.Id, 408 MinCount: 1, 409 MaxCount: 1, 410 UserData: userData, 411 InstanceType: spec.InstanceType.Name, 412 SecurityGroups: groups, 413 BlockDeviceMappings: []ec2.BlockDeviceMapping{device}, 414 }) 415 if err == nil || ec2ErrCode(err) != "InvalidGroup.NotFound" { 416 break 417 } 418 } 419 if err != nil { 420 return nil, nil, fmt.Errorf("cannot run instances: %v", err) 421 } 422 if len(instResp.Instances) != 1 { 423 return nil, nil, fmt.Errorf("expected 1 started instance, got %d", len(instResp.Instances)) 424 } 425 426 inst := &ec2Instance{ 427 e: e, 428 Instance: &instResp.Instances[0], 429 } 430 logger.Infof("started instance %q", inst.Id()) 431 432 hc := instance.HardwareCharacteristics{ 433 Arch: &spec.Image.Arch, 434 Mem: &spec.InstanceType.Mem, 435 CpuCores: &spec.InstanceType.CpuCores, 436 CpuPower: spec.InstanceType.CpuPower, 437 RootDisk: &diskSize, 438 // Tags currently not supported by EC2 439 } 440 return inst, &hc, nil 441 } 442 443 func (e *environ) StopInstances(insts []instance.Instance) error { 444 ids := make([]instance.Id, len(insts)) 445 for i, inst := range insts { 446 ids[i] = inst.(*ec2Instance).Id() 447 } 448 return e.terminateInstances(ids) 449 } 450 451 // minDiskSize is the minimum/default size (in megabytes) for ec2 root disks. 452 const minDiskSize uint64 = 8 * 1024 453 454 // getDiskSize translates a RootDisk constraint (or lackthereof) into a 455 // BlockDeviceMapping request for EC2. megs is the size in megabytes of 456 // the disk that was requested. 457 func getDiskSize(cons constraints.Value) (dvc ec2.BlockDeviceMapping, megs uint64) { 458 diskSize := minDiskSize 459 460 if cons.RootDisk != nil { 461 if *cons.RootDisk >= minDiskSize { 462 diskSize = *cons.RootDisk 463 } else { 464 logger.Infof("Ignoring root-disk constraint of %dM because it is smaller than the EC2 image size of %dM", 465 *cons.RootDisk, minDiskSize) 466 } 467 } 468 469 // AWS's volume size is in gigabytes, root-disk is in megabytes, 470 // so round up to the nearest gigabyte. 471 volsize := int64((diskSize + 1023) / 1024) 472 return ec2.BlockDeviceMapping{ 473 DeviceName: "/dev/sda1", 474 VolumeSize: volsize, 475 }, 476 uint64(volsize * 1024) 477 } 478 479 // groupInfoByName returns information on the security group 480 // with the given name including rules and other details. 481 func (e *environ) groupInfoByName(groupName string) (ec2.SecurityGroupInfo, error) { 482 // Non-default VPC does not support name-based group lookups, can 483 // use a filter by group name instead when support is needed. 484 limitToGroups := []ec2.SecurityGroup{{Name: groupName}} 485 resp, err := e.ec2().SecurityGroups(limitToGroups, nil) 486 if err != nil { 487 return ec2.SecurityGroupInfo{}, err 488 } 489 if len(resp.Groups) != 1 { 490 return ec2.SecurityGroupInfo{}, fmt.Errorf("expected one security group named %q, got %v", groupName, resp.Groups) 491 } 492 return resp.Groups[0], nil 493 } 494 495 // groupByName returns the security group with the given name. 496 func (e *environ) groupByName(groupName string) (ec2.SecurityGroup, error) { 497 groupInfo, err := e.groupInfoByName(groupName) 498 return groupInfo.SecurityGroup, err 499 } 500 501 // addGroupFilter sets a limit an instance filter so only those machines 502 // with the juju environment wide security group associated will be listed. 503 // 504 // An EC2 API call is required to resolve the group name to an id, as VPC 505 // enabled accounts do not support name based filtering. 506 // TODO: Detect classic accounts and just filter by name for those. 507 // 508 // Callers must handle InvalidGroup.NotFound errors to mean the same as no 509 // matching instances. 510 func (e *environ) addGroupFilter(filter *ec2.Filter) error { 511 groupName := e.jujuGroupName() 512 group, err := e.groupByName(groupName) 513 if err != nil { 514 return err 515 } 516 // EC2 should support filtering with and without the 'instance.' 517 // prefix, but only the form with seems to work with default VPC. 518 filter.Add("instance.group-id", group.Id) 519 return nil 520 } 521 522 // gatherInstances tries to get information on each instance 523 // id whose corresponding insts slot is nil. 524 // It returns environs.ErrPartialInstances if the insts 525 // slice has not been completely filled. 526 func (e *environ) gatherInstances(ids []instance.Id, insts []instance.Instance) error { 527 var need []string 528 for i, inst := range insts { 529 if inst == nil { 530 need = append(need, string(ids[i])) 531 } 532 } 533 if len(need) == 0 { 534 return nil 535 } 536 filter := ec2.NewFilter() 537 filter.Add("instance-state-name", "pending", "running") 538 err := e.addGroupFilter(filter) 539 if err != nil { 540 if ec2ErrCode(err) == "InvalidGroup.NotFound" { 541 return environs.ErrPartialInstances 542 } 543 return err 544 } 545 filter.Add("instance-id", need...) 546 resp, err := e.ec2().Instances(nil, filter) 547 if err != nil { 548 return err 549 } 550 n := 0 551 // For each requested id, add it to the returned instances 552 // if we find it in the response. 553 for i, id := range ids { 554 if insts[i] != nil { 555 continue 556 } 557 for j := range resp.Reservations { 558 r := &resp.Reservations[j] 559 for k := range r.Instances { 560 if r.Instances[k].InstanceId == string(id) { 561 inst := r.Instances[k] 562 // TODO(wallyworld): lookup the details to fill in the instance type data 563 insts[i] = &ec2Instance{e: e, Instance: &inst} 564 n++ 565 } 566 } 567 } 568 } 569 if n < len(ids) { 570 return environs.ErrPartialInstances 571 } 572 return nil 573 } 574 575 func (e *environ) Instances(ids []instance.Id) ([]instance.Instance, error) { 576 if len(ids) == 0 { 577 return nil, nil 578 } 579 insts := make([]instance.Instance, len(ids)) 580 // Make a series of requests to cope with eventual consistency. 581 // Each request will attempt to add more instances to the requested 582 // set. 583 var err error 584 for a := shortAttempt.Start(); a.Next(); { 585 err = e.gatherInstances(ids, insts) 586 if err == nil || err != environs.ErrPartialInstances { 587 break 588 } 589 } 590 if err == environs.ErrPartialInstances { 591 for _, inst := range insts { 592 if inst != nil { 593 return insts, environs.ErrPartialInstances 594 } 595 } 596 return nil, environs.ErrNoInstances 597 } 598 if err != nil { 599 return nil, err 600 } 601 return insts, nil 602 } 603 604 func (e *environ) AllInstances() ([]instance.Instance, error) { 605 filter := ec2.NewFilter() 606 filter.Add("instance-state-name", "pending", "running") 607 err := e.addGroupFilter(filter) 608 if err != nil { 609 if ec2ErrCode(err) == "InvalidGroup.NotFound" { 610 return nil, nil 611 } 612 return nil, err 613 } 614 resp, err := e.ec2().Instances(nil, filter) 615 if err != nil { 616 return nil, err 617 } 618 var insts []instance.Instance 619 for _, r := range resp.Reservations { 620 for i := range r.Instances { 621 inst := r.Instances[i] 622 // TODO(wallyworld): lookup the details to fill in the instance type data 623 insts = append(insts, &ec2Instance{e: e, Instance: &inst}) 624 } 625 } 626 return insts, nil 627 } 628 629 func (e *environ) Destroy() error { 630 return common.Destroy(e) 631 } 632 633 func portsToIPPerms(ports []instance.Port) []ec2.IPPerm { 634 ipPerms := make([]ec2.IPPerm, len(ports)) 635 for i, p := range ports { 636 ipPerms[i] = ec2.IPPerm{ 637 Protocol: p.Protocol, 638 FromPort: p.Number, 639 ToPort: p.Number, 640 SourceIPs: []string{"0.0.0.0/0"}, 641 } 642 } 643 return ipPerms 644 } 645 646 func (e *environ) openPortsInGroup(name string, ports []instance.Port) error { 647 if len(ports) == 0 { 648 return nil 649 } 650 // Give permissions for anyone to access the given ports. 651 g, err := e.groupByName(name) 652 if err != nil { 653 return err 654 } 655 ipPerms := portsToIPPerms(ports) 656 _, err = e.ec2().AuthorizeSecurityGroup(g, ipPerms) 657 if err != nil && ec2ErrCode(err) == "InvalidPermission.Duplicate" { 658 if len(ports) == 1 { 659 return nil 660 } 661 // If there's more than one port and we get a duplicate error, 662 // then we go through authorizing each port individually, 663 // otherwise the ports that were *not* duplicates will have 664 // been ignored 665 for i := range ipPerms { 666 _, err := e.ec2().AuthorizeSecurityGroup(g, ipPerms[i:i+1]) 667 if err != nil && ec2ErrCode(err) != "InvalidPermission.Duplicate" { 668 return fmt.Errorf("cannot open port %v: %v", ipPerms[i], err) 669 } 670 } 671 return nil 672 } 673 if err != nil { 674 return fmt.Errorf("cannot open ports: %v", err) 675 } 676 return nil 677 } 678 679 func (e *environ) closePortsInGroup(name string, ports []instance.Port) error { 680 if len(ports) == 0 { 681 return nil 682 } 683 // Revoke permissions for anyone to access the given ports. 684 // Note that ec2 allows the revocation of permissions that aren't 685 // granted, so this is naturally idempotent. 686 g, err := e.groupByName(name) 687 if err != nil { 688 return err 689 } 690 _, err = e.ec2().RevokeSecurityGroup(g, portsToIPPerms(ports)) 691 if err != nil { 692 return fmt.Errorf("cannot close ports: %v", err) 693 } 694 return nil 695 } 696 697 func (e *environ) portsInGroup(name string) (ports []instance.Port, err error) { 698 group, err := e.groupInfoByName(name) 699 if err != nil { 700 return nil, err 701 } 702 for _, p := range group.IPPerms { 703 if len(p.SourceIPs) != 1 { 704 logger.Warningf("unexpected IP permission found: %v", p) 705 continue 706 } 707 for i := p.FromPort; i <= p.ToPort; i++ { 708 ports = append(ports, instance.Port{ 709 Protocol: p.Protocol, 710 Number: i, 711 }) 712 } 713 } 714 instance.SortPorts(ports) 715 return ports, nil 716 } 717 718 func (e *environ) OpenPorts(ports []instance.Port) error { 719 if e.Config().FirewallMode() != config.FwGlobal { 720 return fmt.Errorf("invalid firewall mode %q for opening ports on environment", 721 e.Config().FirewallMode()) 722 } 723 if err := e.openPortsInGroup(e.globalGroupName(), ports); err != nil { 724 return err 725 } 726 logger.Infof("opened ports in global group: %v", ports) 727 return nil 728 } 729 730 func (e *environ) ClosePorts(ports []instance.Port) error { 731 if e.Config().FirewallMode() != config.FwGlobal { 732 return fmt.Errorf("invalid firewall mode %q for closing ports on environment", 733 e.Config().FirewallMode()) 734 } 735 if err := e.closePortsInGroup(e.globalGroupName(), ports); err != nil { 736 return err 737 } 738 logger.Infof("closed ports in global group: %v", ports) 739 return nil 740 } 741 742 func (e *environ) Ports() ([]instance.Port, error) { 743 if e.Config().FirewallMode() != config.FwGlobal { 744 return nil, fmt.Errorf("invalid firewall mode %q for retrieving ports from environment", 745 e.Config().FirewallMode()) 746 } 747 return e.portsInGroup(e.globalGroupName()) 748 } 749 750 func (*environ) Provider() environs.EnvironProvider { 751 return &providerInstance 752 } 753 754 func (e *environ) terminateInstances(ids []instance.Id) error { 755 if len(ids) == 0 { 756 return nil 757 } 758 var err error 759 ec2inst := e.ec2() 760 strs := make([]string, len(ids)) 761 for i, id := range ids { 762 strs[i] = string(id) 763 } 764 for a := shortAttempt.Start(); a.Next(); { 765 _, err = ec2inst.TerminateInstances(strs) 766 if err == nil || ec2ErrCode(err) != "InvalidInstanceID.NotFound" { 767 return err 768 } 769 } 770 if len(ids) == 1 { 771 return err 772 } 773 // If we get a NotFound error, it means that no instances have been 774 // terminated even if some exist, so try them one by one, ignoring 775 // NotFound errors. 776 var firstErr error 777 for _, id := range ids { 778 _, err = ec2inst.TerminateInstances([]string{string(id)}) 779 if ec2ErrCode(err) == "InvalidInstanceID.NotFound" { 780 err = nil 781 } 782 if err != nil && firstErr == nil { 783 firstErr = err 784 } 785 } 786 return firstErr 787 } 788 789 func (e *environ) globalGroupName() string { 790 return fmt.Sprintf("%s-global", e.jujuGroupName()) 791 } 792 793 func (e *environ) machineGroupName(machineId string) string { 794 return fmt.Sprintf("%s-%s", e.jujuGroupName(), machineId) 795 } 796 797 func (e *environ) jujuGroupName() string { 798 return "juju-" + e.name 799 } 800 801 func (inst *ec2Instance) OpenPorts(machineId string, ports []instance.Port) error { 802 if inst.e.Config().FirewallMode() != config.FwInstance { 803 return fmt.Errorf("invalid firewall mode %q for opening ports on instance", 804 inst.e.Config().FirewallMode()) 805 } 806 name := inst.e.machineGroupName(machineId) 807 if err := inst.e.openPortsInGroup(name, ports); err != nil { 808 return err 809 } 810 logger.Infof("opened ports in security group %s: %v", name, ports) 811 return nil 812 } 813 814 func (inst *ec2Instance) ClosePorts(machineId string, ports []instance.Port) error { 815 if inst.e.Config().FirewallMode() != config.FwInstance { 816 return fmt.Errorf("invalid firewall mode %q for closing ports on instance", 817 inst.e.Config().FirewallMode()) 818 } 819 name := inst.e.machineGroupName(machineId) 820 if err := inst.e.closePortsInGroup(name, ports); err != nil { 821 return err 822 } 823 logger.Infof("closed ports in security group %s: %v", name, ports) 824 return nil 825 } 826 827 func (inst *ec2Instance) Ports(machineId string) ([]instance.Port, error) { 828 if inst.e.Config().FirewallMode() != config.FwInstance { 829 return nil, fmt.Errorf("invalid firewall mode %q for retrieving ports from instance", 830 inst.e.Config().FirewallMode()) 831 } 832 name := inst.e.machineGroupName(machineId) 833 return inst.e.portsInGroup(name) 834 } 835 836 // setUpGroups creates the security groups for the new machine, and 837 // returns them. 838 // 839 // Instances are tagged with a group so they can be distinguished from 840 // other instances that might be running on the same EC2 account. In 841 // addition, a specific machine security group is created for each 842 // machine, so that its firewall rules can be configured per machine. 843 func (e *environ) setUpGroups(machineId string, statePort, apiPort int) ([]ec2.SecurityGroup, error) { 844 jujuGroup, err := e.ensureGroup(e.jujuGroupName(), 845 []ec2.IPPerm{ 846 { 847 Protocol: "tcp", 848 FromPort: 22, 849 ToPort: 22, 850 SourceIPs: []string{"0.0.0.0/0"}, 851 }, 852 { 853 Protocol: "tcp", 854 FromPort: statePort, 855 ToPort: statePort, 856 SourceIPs: []string{"0.0.0.0/0"}, 857 }, 858 { 859 Protocol: "tcp", 860 FromPort: apiPort, 861 ToPort: apiPort, 862 SourceIPs: []string{"0.0.0.0/0"}, 863 }, 864 { 865 Protocol: "tcp", 866 FromPort: 0, 867 ToPort: 65535, 868 }, 869 { 870 Protocol: "udp", 871 FromPort: 0, 872 ToPort: 65535, 873 }, 874 { 875 Protocol: "icmp", 876 FromPort: -1, 877 ToPort: -1, 878 }, 879 }) 880 if err != nil { 881 return nil, err 882 } 883 var machineGroup ec2.SecurityGroup 884 switch e.Config().FirewallMode() { 885 case config.FwInstance: 886 machineGroup, err = e.ensureGroup(e.machineGroupName(machineId), nil) 887 case config.FwGlobal: 888 machineGroup, err = e.ensureGroup(e.globalGroupName(), nil) 889 } 890 if err != nil { 891 return nil, err 892 } 893 return []ec2.SecurityGroup{jujuGroup, machineGroup}, nil 894 } 895 896 // zeroGroup holds the zero security group. 897 var zeroGroup ec2.SecurityGroup 898 899 // ensureGroup returns the security group with name and perms. 900 // If a group with name does not exist, one will be created. 901 // If it exists, its permissions are set to perms. 902 // Any entries in perms without SourceIPs will be granted for 903 // the named group only. 904 func (e *environ) ensureGroup(name string, perms []ec2.IPPerm) (g ec2.SecurityGroup, err error) { 905 ec2inst := e.ec2() 906 resp, err := ec2inst.CreateSecurityGroup(name, "juju group") 907 if err != nil && ec2ErrCode(err) != "InvalidGroup.Duplicate" { 908 return zeroGroup, err 909 } 910 911 var have permSet 912 if err == nil { 913 g = resp.SecurityGroup 914 } else { 915 resp, err := ec2inst.SecurityGroups(ec2.SecurityGroupNames(name), nil) 916 if err != nil { 917 return zeroGroup, err 918 } 919 info := resp.Groups[0] 920 // It's possible that the old group has the wrong 921 // description here, but if it does it's probably due 922 // to something deliberately playing games with juju, 923 // so we ignore it. 924 g = info.SecurityGroup 925 have = newPermSetForGroup(info.IPPerms, g) 926 } 927 want := newPermSetForGroup(perms, g) 928 revoke := make(permSet) 929 for p := range have { 930 if !want[p] { 931 revoke[p] = true 932 } 933 } 934 if len(revoke) > 0 { 935 _, err := ec2inst.RevokeSecurityGroup(g, revoke.ipPerms()) 936 if err != nil { 937 return zeroGroup, fmt.Errorf("cannot revoke security group: %v", err) 938 } 939 } 940 941 add := make(permSet) 942 for p := range want { 943 if !have[p] { 944 add[p] = true 945 } 946 } 947 if len(add) > 0 { 948 _, err := ec2inst.AuthorizeSecurityGroup(g, add.ipPerms()) 949 if err != nil { 950 return zeroGroup, fmt.Errorf("cannot authorize securityGroup: %v", err) 951 } 952 } 953 return g, nil 954 } 955 956 // permKey represents a permission for a group or an ip address range 957 // to access the given range of ports. Only one of groupName or ipAddr 958 // should be non-empty. 959 type permKey struct { 960 protocol string 961 fromPort int 962 toPort int 963 groupId string 964 ipAddr string 965 } 966 967 type permSet map[permKey]bool 968 969 // newPermSetForGroup returns a set of all the permissions in the 970 // given slice of IPPerms. It ignores the name and owner 971 // id in source groups, and any entry with no source ips will 972 // be granted for the given group only. 973 func newPermSetForGroup(ps []ec2.IPPerm, group ec2.SecurityGroup) permSet { 974 m := make(permSet) 975 for _, p := range ps { 976 k := permKey{ 977 protocol: p.Protocol, 978 fromPort: p.FromPort, 979 toPort: p.ToPort, 980 } 981 if len(p.SourceIPs) > 0 { 982 for _, ip := range p.SourceIPs { 983 k.ipAddr = ip 984 m[k] = true 985 } 986 } else { 987 k.groupId = group.Id 988 m[k] = true 989 } 990 } 991 return m 992 } 993 994 // ipPerms returns m as a slice of permissions usable 995 // with the ec2 package. 996 func (m permSet) ipPerms() (ps []ec2.IPPerm) { 997 // We could compact the permissions, but it 998 // hardly seems worth it. 999 for p := range m { 1000 ipp := ec2.IPPerm{ 1001 Protocol: p.protocol, 1002 FromPort: p.fromPort, 1003 ToPort: p.toPort, 1004 } 1005 if p.ipAddr != "" { 1006 ipp.SourceIPs = []string{p.ipAddr} 1007 } else { 1008 ipp.SourceGroups = []ec2.UserSecurityGroup{{Id: p.groupId}} 1009 } 1010 ps = append(ps, ipp) 1011 } 1012 return 1013 } 1014 1015 // If the err is of type *ec2.Error, ec2ErrCode returns 1016 // its code, otherwise it returns the empty string. 1017 func ec2ErrCode(err error) string { 1018 ec2err, _ := err.(*ec2.Error) 1019 if ec2err == nil { 1020 return "" 1021 } 1022 return ec2err.Code 1023 } 1024 1025 // metadataHost holds the address of the instance metadata service. 1026 // It is a variable so that tests can change it to refer to a local 1027 // server when needed. 1028 var metadataHost = "http://169.254.169.254" 1029 1030 // fetchMetadata fetches a single atom of data from the ec2 instance metadata service. 1031 // http://docs.amazonwebservices.com/AWSEC2/latest/UserGuide/AESDG-chapter-instancedata.html 1032 func fetchMetadata(name string) (value string, err error) { 1033 uri := fmt.Sprintf("%s/2011-01-01/meta-data/%s", metadataHost, name) 1034 defer utils.ErrorContextf(&err, "cannot get %q", uri) 1035 for a := shortAttempt.Start(); a.Next(); { 1036 var resp *http.Response 1037 resp, err = http.Get(uri) 1038 if err != nil { 1039 continue 1040 } 1041 defer resp.Body.Close() 1042 if resp.StatusCode != http.StatusOK { 1043 err = fmt.Errorf("bad http response %v", resp.Status) 1044 continue 1045 } 1046 var data []byte 1047 data, err = ioutil.ReadAll(resp.Body) 1048 if err != nil { 1049 continue 1050 } 1051 return strings.TrimSpace(string(data)), nil 1052 } 1053 return 1054 } 1055 1056 // GetImageSources returns a list of sources which are used to search for simplestreams image metadata. 1057 func (e *environ) GetImageSources() ([]simplestreams.DataSource, error) { 1058 // Add the simplestreams source off the control bucket. 1059 sources := []simplestreams.DataSource{ 1060 storage.NewStorageSimpleStreamsDataSource("cloud storage", e.Storage(), storage.BaseImagesPath)} 1061 return sources, nil 1062 } 1063 1064 // GetToolsSources returns a list of sources which are used to search for simplestreams tools metadata. 1065 func (e *environ) GetToolsSources() ([]simplestreams.DataSource, error) { 1066 // Add the simplestreams source off the control bucket. 1067 sources := []simplestreams.DataSource{ 1068 storage.NewStorageSimpleStreamsDataSource("cloud storage", e.Storage(), storage.BaseToolsPath)} 1069 return sources, nil 1070 }