github.com/rogpeppe/juju@v0.0.0-20140613142852-6337964b789e/provider/openstack/provider.go (about) 1 // Copyright 2012, 2013 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 4 // Stub provider for OpenStack, using goose will be implemented here 5 6 package openstack 7 8 import ( 9 "fmt" 10 "io/ioutil" 11 "net/http" 12 "regexp" 13 "strings" 14 "sync" 15 "time" 16 17 jujuerrors "github.com/juju/errors" 18 "github.com/juju/loggo" 19 "github.com/juju/names" 20 "github.com/juju/utils" 21 "launchpad.net/goose/client" 22 gooseerrors "launchpad.net/goose/errors" 23 "launchpad.net/goose/identity" 24 "launchpad.net/goose/nova" 25 "launchpad.net/goose/swift" 26 27 "github.com/juju/juju/constraints" 28 "github.com/juju/juju/environs" 29 "github.com/juju/juju/environs/config" 30 "github.com/juju/juju/environs/imagemetadata" 31 "github.com/juju/juju/environs/instances" 32 "github.com/juju/juju/environs/simplestreams" 33 "github.com/juju/juju/environs/storage" 34 envtools "github.com/juju/juju/environs/tools" 35 "github.com/juju/juju/instance" 36 "github.com/juju/juju/juju/arch" 37 "github.com/juju/juju/network" 38 "github.com/juju/juju/provider/common" 39 "github.com/juju/juju/state" 40 "github.com/juju/juju/state/api" 41 "github.com/juju/juju/tools" 42 ) 43 44 var logger = loggo.GetLogger("juju.provider.openstack") 45 46 type environProvider struct{} 47 48 var _ environs.EnvironProvider = (*environProvider)(nil) 49 50 var providerInstance environProvider 51 52 // Use shortAttempt to poll for short-term events. 53 // TODO: This was kept to a long timeout because Nova needs more time than EC2. 54 // For example, HP Cloud takes around 9.1 seconds (10 samples) to return a 55 // BUILD(spawning) status. But storage delays are handled separately now, and 56 // perhaps other polling attempts can time out faster. 57 var shortAttempt = utils.AttemptStrategy{ 58 Total: 15 * time.Second, 59 Delay: 200 * time.Millisecond, 60 } 61 62 func init() { 63 environs.RegisterProvider("openstack", environProvider{}) 64 } 65 66 func (p environProvider) BoilerplateConfig() string { 67 return ` 68 # https://juju.ubuntu.com/docs/config-openstack.html 69 openstack: 70 type: openstack 71 72 # use-floating-ip specifies whether a floating IP address is 73 # required to give the nodes a public IP address. Some 74 # installations assign public IP addresses by default without 75 # requiring a floating IP address. 76 # 77 # use-floating-ip: false 78 79 # use-default-secgroup specifies whether new machine instances 80 # should have the "default" Openstack security group assigned. 81 # 82 # use-default-secgroup: false 83 84 # network specifies the network label or uuid to bring machines up 85 # on, in the case where multiple networks exist. It may be omitted 86 # otherwise. 87 # 88 # network: <your network label or uuid> 89 90 # tools-metadata-url specifies the location of the Juju tools and 91 # metadata. It defaults to the global public tools metadata 92 # location https://streams.canonical.com/tools. 93 # 94 # tools-metadata-url: https://your-tools-metadata-url 95 96 # image-metadata-url specifies the location of Ubuntu cloud image 97 # metadata. It defaults to the global public image metadata 98 # location https://cloud-images.ubuntu.com/releases. 99 # 100 # image-metadata-url: https://your-image-metadata-url 101 102 # image-stream chooses a simplestreams stream to select OS images 103 # from, for example daily or released images (or any other stream 104 # available on simplestreams). 105 # 106 # image-stream: "released" 107 108 # auth-url defaults to the value of the environment variable 109 # OS_AUTH_URL, but can be specified here. 110 # 111 # auth-url: https://yourkeystoneurl:443/v2.0/ 112 113 # tenant-name holds the openstack tenant name. It defaults to the 114 # environment variable OS_TENANT_NAME. 115 # 116 # tenant-name: <your tenant name> 117 118 # region holds the openstack region. It defaults to the 119 # environment variable OS_REGION_NAME. 120 # 121 # region: <your region> 122 123 # The auth-mode, username and password attributes are used for 124 # userpass authentication (the default). 125 # 126 # auth-mode holds the authentication mode. For user-password 127 # authentication, auth-mode should be "userpass" and username and 128 # password should be set appropriately; they default to the 129 # environment variables OS_USERNAME and OS_PASSWORD respectively. 130 # 131 # auth-mode: userpass 132 # username: <your username> 133 # password: <secret> 134 135 # For key-pair authentication, auth-mode should be "keypair" and 136 # access-key and secret-key should be set appropriately; they 137 # default to the environment variables OS_ACCESS_KEY and 138 # OS_SECRET_KEY respectively. 139 # 140 # auth-mode: keypair 141 # access-key: <secret> 142 # secret-key: <secret> 143 144 # https://juju.ubuntu.com/docs/config-hpcloud.html 145 hpcloud: 146 type: openstack 147 148 # use-floating-ip specifies whether a floating IP address is 149 # required to give the nodes a public IP address. Some 150 # installations assign public IP addresses by default without 151 # requiring a floating IP address. 152 # 153 # use-floating-ip: true 154 155 # use-default-secgroup specifies whether new machine instances 156 # should have the "default" Openstack security group assigned. 157 # 158 # use-default-secgroup: false 159 160 # tenant-name holds the openstack tenant name. In HPCloud, this is 161 # synonymous with the project-name It defaults to the environment 162 # variable OS_TENANT_NAME. 163 # 164 # tenant-name: <your tenant name> 165 166 # image-stream chooses a simplestreams stream to select OS images 167 # from, for example daily or released images (or any other stream 168 # available on simplestreams). 169 # 170 # image-stream: "released" 171 172 # auth-url holds the keystone url for authentication. It defaults 173 # to the value of the environment variable OS_AUTH_URL. 174 # 175 # auth-url: https://region-a.geo-1.identity.hpcloudsvc.com:35357/v2.0/ 176 177 # region holds the HP Cloud region (e.g. region-a.geo-1). It 178 # defaults to the environment variable OS_REGION_NAME. 179 # 180 # region: <your region> 181 182 # auth-mode holds the authentication mode. For user-password 183 # authentication, auth-mode should be "userpass" and username and 184 # password should be set appropriately; they default to the 185 # environment variables OS_USERNAME and OS_PASSWORD respectively. 186 # 187 # auth-mode: userpass 188 # username: <your_username> 189 # password: <your_password> 190 191 # For key-pair authentication, auth-mode should be "keypair" and 192 # access-key and secret-key should be set appropriately; they 193 # default to the environment variables OS_ACCESS_KEY and 194 # OS_SECRET_KEY respectively. 195 # 196 # auth-mode: keypair 197 # access-key: <secret> 198 # secret-key: <secret> 199 200 `[1:] 201 } 202 203 func (p environProvider) Open(cfg *config.Config) (environs.Environ, error) { 204 logger.Infof("opening environment %q", cfg.Name()) 205 e := new(environ) 206 err := e.SetConfig(cfg) 207 if err != nil { 208 return nil, err 209 } 210 e.name = cfg.Name() 211 return e, nil 212 } 213 214 func (p environProvider) Prepare(ctx environs.BootstrapContext, cfg *config.Config) (environs.Environ, error) { 215 attrs := cfg.UnknownAttrs() 216 if _, ok := attrs["control-bucket"]; !ok { 217 uuid, err := utils.NewUUID() 218 if err != nil { 219 return nil, err 220 } 221 attrs["control-bucket"] = fmt.Sprintf("%x", uuid.Raw()) 222 } 223 cfg, err := cfg.Apply(attrs) 224 if err != nil { 225 return nil, err 226 } 227 return p.Open(cfg) 228 } 229 230 // MetadataLookupParams returns parameters which are used to query image metadata to 231 // find matching image information. 232 func (p environProvider) MetadataLookupParams(region string) (*simplestreams.MetadataLookupParams, error) { 233 if region == "" { 234 return nil, fmt.Errorf("region must be specified") 235 } 236 return &simplestreams.MetadataLookupParams{ 237 Region: region, 238 Architectures: arch.AllSupportedArches, 239 }, nil 240 } 241 242 func (p environProvider) SecretAttrs(cfg *config.Config) (map[string]string, error) { 243 m := make(map[string]string) 244 ecfg, err := providerInstance.newConfig(cfg) 245 if err != nil { 246 return nil, err 247 } 248 m["username"] = ecfg.username() 249 m["password"] = ecfg.password() 250 m["tenant-name"] = ecfg.tenantName() 251 return m, nil 252 } 253 254 func retryGet(uri string) (data []byte, err error) { 255 for a := shortAttempt.Start(); a.Next(); { 256 var resp *http.Response 257 resp, err = http.Get(uri) 258 if err != nil { 259 continue 260 } 261 defer resp.Body.Close() 262 if resp.StatusCode != http.StatusOK { 263 err = fmt.Errorf("bad http response %v", resp.Status) 264 continue 265 } 266 var data []byte 267 data, err = ioutil.ReadAll(resp.Body) 268 if err != nil { 269 continue 270 } 271 return data, nil 272 } 273 if err != nil { 274 return nil, fmt.Errorf("cannot get %q: %v", uri, err) 275 } 276 return 277 } 278 279 type environ struct { 280 common.SupportsUnitPlacementPolicy 281 282 name string 283 284 // archMutex gates access to supportedArchitectures 285 archMutex sync.Mutex 286 // supportedArchitectures caches the architectures 287 // for which images can be instantiated. 288 supportedArchitectures []string 289 290 ecfgMutex sync.Mutex 291 imageBaseMutex sync.Mutex 292 toolsBaseMutex sync.Mutex 293 ecfgUnlocked *environConfig 294 client client.AuthenticatingClient 295 novaUnlocked *nova.Client 296 storageUnlocked storage.Storage 297 // An ordered list of sources in which to find the simplestreams index files used to 298 // look up image ids. 299 imageSources []simplestreams.DataSource 300 // An ordered list of paths in which to find the simplestreams index files used to 301 // look up tools ids. 302 toolsSources []simplestreams.DataSource 303 304 availabilityZonesMutex sync.Mutex 305 availabilityZones []common.AvailabilityZone 306 } 307 308 var _ environs.Environ = (*environ)(nil) 309 var _ imagemetadata.SupportsCustomSources = (*environ)(nil) 310 var _ envtools.SupportsCustomSources = (*environ)(nil) 311 var _ simplestreams.HasRegion = (*environ)(nil) 312 var _ state.Prechecker = (*environ)(nil) 313 var _ state.InstanceDistributor = (*environ)(nil) 314 315 type openstackInstance struct { 316 e *environ 317 instType *instances.InstanceType 318 arch *string 319 320 mu sync.Mutex 321 serverDetail *nova.ServerDetail 322 } 323 324 func (inst *openstackInstance) String() string { 325 return string(inst.Id()) 326 } 327 328 var _ instance.Instance = (*openstackInstance)(nil) 329 330 func (inst *openstackInstance) Refresh() error { 331 inst.mu.Lock() 332 defer inst.mu.Unlock() 333 server, err := inst.e.nova().GetServer(inst.serverDetail.Id) 334 if err != nil { 335 return err 336 } 337 inst.serverDetail = server 338 return nil 339 } 340 341 func (inst *openstackInstance) getServerDetail() *nova.ServerDetail { 342 inst.mu.Lock() 343 defer inst.mu.Unlock() 344 return inst.serverDetail 345 } 346 347 func (inst *openstackInstance) Id() instance.Id { 348 return instance.Id(inst.getServerDetail().Id) 349 } 350 351 func (inst *openstackInstance) Status() string { 352 return inst.getServerDetail().Status 353 } 354 355 func (inst *openstackInstance) hardwareCharacteristics() *instance.HardwareCharacteristics { 356 hc := &instance.HardwareCharacteristics{Arch: inst.arch} 357 if inst.instType != nil { 358 hc.Mem = &inst.instType.Mem 359 // openstack is special in that a 0-size root disk means that 360 // the root disk will result in an instance with a root disk 361 // the same size as the image that created it, so we just set 362 // the HardwareCharacteristics to nil to signal that we don't 363 // know what the correct size is. 364 if inst.instType.RootDisk == 0 { 365 hc.RootDisk = nil 366 } else { 367 hc.RootDisk = &inst.instType.RootDisk 368 } 369 hc.CpuCores = &inst.instType.CpuCores 370 hc.CpuPower = inst.instType.CpuPower 371 // tags not currently supported on openstack 372 } 373 return hc 374 } 375 376 // getAddresses returns the existing server information on addresses, 377 // but fetches the details over the api again if no addresses exist. 378 func (inst *openstackInstance) getAddresses() (map[string][]nova.IPAddress, error) { 379 addrs := inst.getServerDetail().Addresses 380 if len(addrs) == 0 { 381 server, err := inst.e.nova().GetServer(string(inst.Id())) 382 if err != nil { 383 return nil, err 384 } 385 addrs = server.Addresses 386 } 387 return addrs, nil 388 } 389 390 // Addresses implements network.Addresses() returning generic address 391 // details for the instances, and calling the openstack api if needed. 392 func (inst *openstackInstance) Addresses() ([]network.Address, error) { 393 addresses, err := inst.getAddresses() 394 if err != nil { 395 return nil, err 396 } 397 return convertNovaAddresses(addresses), nil 398 } 399 400 // convertNovaAddresses returns nova addresses in generic format 401 func convertNovaAddresses(addresses map[string][]nova.IPAddress) []network.Address { 402 // TODO(gz) Network ordering may be significant but is not preserved by 403 // the map, see lp:1188126 for example. That could potentially be fixed 404 // in goose, or left to be derived by other means. 405 var machineAddresses []network.Address 406 for netName, ips := range addresses { 407 networkScope := network.ScopeUnknown 408 // For canonistack and hpcloud, public floating addresses may 409 // be put in networks named something other than public. Rely 410 // on address sanity logic to catch and mark them corectly. 411 if netName == "public" { 412 networkScope = network.ScopePublic 413 } 414 for _, address := range ips { 415 // Assume IPv4 unless specified otherwise 416 addrtype := network.IPv4Address 417 if address.Version == 6 { 418 addrtype = network.IPv6Address 419 } 420 machineAddr := network.NewAddress(address.Address, networkScope) 421 machineAddr.NetworkName = netName 422 if machineAddr.Type != addrtype { 423 logger.Warningf("derived address type %v, nova reports %v", machineAddr.Type, addrtype) 424 } 425 machineAddresses = append(machineAddresses, machineAddr) 426 } 427 } 428 return machineAddresses 429 } 430 431 // TODO: following 30 lines nearly verbatim from environs/ec2 432 433 func (inst *openstackInstance) OpenPorts(machineId string, ports []network.Port) error { 434 if inst.e.Config().FirewallMode() != config.FwInstance { 435 return fmt.Errorf("invalid firewall mode %q for opening ports on instance", 436 inst.e.Config().FirewallMode()) 437 } 438 name := inst.e.machineGroupName(machineId) 439 if err := inst.e.openPortsInGroup(name, ports); err != nil { 440 return err 441 } 442 logger.Infof("opened ports in security group %s: %v", name, ports) 443 return nil 444 } 445 446 func (inst *openstackInstance) ClosePorts(machineId string, ports []network.Port) error { 447 if inst.e.Config().FirewallMode() != config.FwInstance { 448 return fmt.Errorf("invalid firewall mode %q for closing ports on instance", 449 inst.e.Config().FirewallMode()) 450 } 451 name := inst.e.machineGroupName(machineId) 452 if err := inst.e.closePortsInGroup(name, ports); err != nil { 453 return err 454 } 455 logger.Infof("closed ports in security group %s: %v", name, ports) 456 return nil 457 } 458 459 func (inst *openstackInstance) Ports(machineId string) ([]network.Port, error) { 460 if inst.e.Config().FirewallMode() != config.FwInstance { 461 return nil, fmt.Errorf("invalid firewall mode %q for retrieving ports from instance", 462 inst.e.Config().FirewallMode()) 463 } 464 name := inst.e.machineGroupName(machineId) 465 return inst.e.portsInGroup(name) 466 } 467 468 func (e *environ) ecfg() *environConfig { 469 e.ecfgMutex.Lock() 470 ecfg := e.ecfgUnlocked 471 e.ecfgMutex.Unlock() 472 return ecfg 473 } 474 475 func (e *environ) nova() *nova.Client { 476 e.ecfgMutex.Lock() 477 nova := e.novaUnlocked 478 e.ecfgMutex.Unlock() 479 return nova 480 } 481 482 func (e *environ) Name() string { 483 return e.name 484 } 485 486 // SupportedArchitectures is specified on the EnvironCapability interface. 487 func (e *environ) SupportedArchitectures() ([]string, error) { 488 e.archMutex.Lock() 489 defer e.archMutex.Unlock() 490 if e.supportedArchitectures != nil { 491 return e.supportedArchitectures, nil 492 } 493 // Create a filter to get all images from our region and for the correct stream. 494 cloudSpec, err := e.Region() 495 if err != nil { 496 return nil, err 497 } 498 imageConstraint := imagemetadata.NewImageConstraint(simplestreams.LookupParams{ 499 CloudSpec: cloudSpec, 500 Stream: e.Config().ImageStream(), 501 }) 502 e.supportedArchitectures, err = common.SupportedArchitectures(e, imageConstraint) 503 return e.supportedArchitectures, err 504 } 505 506 // SupportNetworks is specified on the EnvironCapability interface. 507 func (e *environ) SupportNetworks() bool { 508 // TODO(dimitern) Once we have support for networking, inquire 509 // about capabilities and return true if supported. 510 return false 511 } 512 513 var unsupportedConstraints = []string{ 514 constraints.Tags, 515 constraints.CpuPower, 516 } 517 518 // ConstraintsValidator is defined on the Environs interface. 519 func (e *environ) ConstraintsValidator() (constraints.Validator, error) { 520 validator := constraints.NewValidator() 521 validator.RegisterConflicts( 522 []string{constraints.InstanceType}, 523 []string{constraints.Mem, constraints.Arch, constraints.RootDisk, constraints.CpuCores}) 524 validator.RegisterUnsupported(unsupportedConstraints) 525 supportedArches, err := e.SupportedArchitectures() 526 if err != nil { 527 return nil, err 528 } 529 validator.RegisterVocabulary(constraints.Arch, supportedArches) 530 novaClient := e.nova() 531 flavors, err := novaClient.ListFlavorsDetail() 532 if err != nil { 533 return nil, err 534 } 535 instTypeNames := make([]string, len(flavors)) 536 for i, flavor := range flavors { 537 instTypeNames[i] = flavor.Name 538 } 539 validator.RegisterVocabulary(constraints.InstanceType, instTypeNames) 540 return validator, nil 541 } 542 543 var novaListAvailabilityZones = (*nova.Client).ListAvailabilityZones 544 545 type openstackAvailabilityZone struct { 546 nova.AvailabilityZone 547 } 548 549 func (z *openstackAvailabilityZone) Name() string { 550 return z.AvailabilityZone.Name 551 } 552 553 func (z *openstackAvailabilityZone) Available() bool { 554 return z.AvailabilityZone.State.Available 555 } 556 557 // AvailabilityZones returns a slice of availability zones. 558 func (e *environ) AvailabilityZones() ([]common.AvailabilityZone, error) { 559 e.availabilityZonesMutex.Lock() 560 defer e.availabilityZonesMutex.Unlock() 561 if e.availabilityZones == nil { 562 zones, err := novaListAvailabilityZones(e.nova()) 563 if err != nil { 564 return nil, err 565 } 566 e.availabilityZones = make([]common.AvailabilityZone, len(zones)) 567 for i, z := range zones { 568 e.availabilityZones[i] = &openstackAvailabilityZone{z} 569 } 570 } 571 return e.availabilityZones, nil 572 } 573 574 // InstanceAvailabilityZoneNames returns the availability zone names for each 575 // of the specified instances. 576 func (e *environ) InstanceAvailabilityZoneNames(ids []instance.Id) ([]string, error) { 577 instances, err := e.Instances(ids) 578 if err != nil && err != environs.ErrPartialInstances { 579 return nil, err 580 } 581 zones := make([]string, len(instances)) 582 for i, inst := range instances { 583 if inst == nil { 584 continue 585 } 586 zones[i] = inst.(*openstackInstance).serverDetail.AvailabilityZone 587 } 588 return zones, err 589 } 590 591 type openstackPlacement struct { 592 availabilityZone nova.AvailabilityZone 593 } 594 595 func (e *environ) parsePlacement(placement string) (*openstackPlacement, error) { 596 pos := strings.IndexRune(placement, '=') 597 if pos == -1 { 598 return nil, fmt.Errorf("unknown placement directive: %v", placement) 599 } 600 switch key, value := placement[:pos], placement[pos+1:]; key { 601 case "zone": 602 availabilityZone := value 603 zones, err := e.AvailabilityZones() 604 if err != nil { 605 return nil, err 606 } 607 for _, z := range zones { 608 if z.Name() == availabilityZone { 609 return &openstackPlacement{ 610 z.(*openstackAvailabilityZone).AvailabilityZone, 611 }, nil 612 } 613 } 614 return nil, fmt.Errorf("invalid availability zone %q", availabilityZone) 615 } 616 return nil, fmt.Errorf("unknown placement directive: %v", placement) 617 } 618 619 // PrecheckInstance is defined on the state.Prechecker interface. 620 func (e *environ) PrecheckInstance(series string, cons constraints.Value, placement string) error { 621 if placement != "" { 622 if _, err := e.parsePlacement(placement); err != nil { 623 return err 624 } 625 } 626 if !cons.HasInstanceType() { 627 return nil 628 } 629 // Constraint has an instance-type constraint so let's see if it is valid. 630 novaClient := e.nova() 631 flavors, err := novaClient.ListFlavorsDetail() 632 if err != nil { 633 return err 634 } 635 for _, flavor := range flavors { 636 if flavor.Name == *cons.InstanceType { 637 return nil 638 } 639 } 640 return fmt.Errorf("invalid Openstack flavour %q specified", *cons.InstanceType) 641 } 642 643 func (e *environ) Storage() storage.Storage { 644 e.ecfgMutex.Lock() 645 stor := e.storageUnlocked 646 e.ecfgMutex.Unlock() 647 return stor 648 } 649 650 func (e *environ) Bootstrap(ctx environs.BootstrapContext, args environs.BootstrapParams) error { 651 // The client's authentication may have been reset when finding tools if the agent-version 652 // attribute was updated so we need to re-authenticate. This will be a no-op if already authenticated. 653 // An authenticated client is needed for the URL() call below. 654 err := e.client.Authenticate() 655 if err != nil { 656 return err 657 } 658 return common.Bootstrap(ctx, e, args) 659 } 660 661 func (e *environ) StateInfo() (*state.Info, *api.Info, error) { 662 return common.StateInfo(e) 663 } 664 665 func (e *environ) Config() *config.Config { 666 return e.ecfg().Config 667 } 668 669 func (e *environ) authClient(ecfg *environConfig, authModeCfg AuthMode) client.AuthenticatingClient { 670 cred := &identity.Credentials{ 671 User: ecfg.username(), 672 Secrets: ecfg.password(), 673 Region: ecfg.region(), 674 TenantName: ecfg.tenantName(), 675 URL: ecfg.authURL(), 676 } 677 // authModeCfg has already been validated so we know it's one of the values below. 678 var authMode identity.AuthMode 679 switch authModeCfg { 680 case AuthLegacy: 681 authMode = identity.AuthLegacy 682 case AuthUserPass: 683 authMode = identity.AuthUserPass 684 case AuthKeyPair: 685 authMode = identity.AuthKeyPair 686 cred.User = ecfg.accessKey() 687 cred.Secrets = ecfg.secretKey() 688 } 689 newClient := client.NewClient 690 if !ecfg.SSLHostnameVerification() { 691 newClient = client.NewNonValidatingClient 692 } 693 return newClient(cred, authMode, nil) 694 } 695 696 func (e *environ) SetConfig(cfg *config.Config) error { 697 ecfg, err := providerInstance.newConfig(cfg) 698 if err != nil { 699 return err 700 } 701 // At this point, the authentication method config value has been validated so we extract it's value here 702 // to avoid having to validate again each time when creating the OpenStack client. 703 var authModeCfg AuthMode 704 e.ecfgMutex.Lock() 705 defer e.ecfgMutex.Unlock() 706 authModeCfg = AuthMode(ecfg.authMode()) 707 e.ecfgUnlocked = ecfg 708 709 e.client = e.authClient(ecfg, authModeCfg) 710 e.novaUnlocked = nova.New(e.client) 711 712 // create new control storage instance, existing instances continue 713 // to reference their existing configuration. 714 // public storage instance creation is deferred until needed since authenticated 715 // access to the identity service is required so that any juju-tools endpoint can be used. 716 e.storageUnlocked = &openstackstorage{ 717 containerName: ecfg.controlBucket(), 718 // this is possibly just a hack - if the ACL is swift.Private, 719 // the machine won't be able to get the tools (401 error) 720 containerACL: swift.PublicRead, 721 swift: swift.New(e.client)} 722 return nil 723 } 724 725 // GetImageSources returns a list of sources which are used to search for simplestreams image metadata. 726 func (e *environ) GetImageSources() ([]simplestreams.DataSource, error) { 727 e.imageBaseMutex.Lock() 728 defer e.imageBaseMutex.Unlock() 729 730 if e.imageSources != nil { 731 return e.imageSources, nil 732 } 733 if !e.client.IsAuthenticated() { 734 err := e.client.Authenticate() 735 if err != nil { 736 return nil, err 737 } 738 } 739 // Add the simplestreams source off the control bucket. 740 e.imageSources = append(e.imageSources, storage.NewStorageSimpleStreamsDataSource( 741 "cloud storage", e.Storage(), storage.BaseImagesPath)) 742 // Add the simplestreams base URL from keystone if it is defined. 743 productStreamsURL, err := e.client.MakeServiceURL("product-streams", nil) 744 if err == nil { 745 verify := utils.VerifySSLHostnames 746 if !e.Config().SSLHostnameVerification() { 747 verify = utils.NoVerifySSLHostnames 748 } 749 source := simplestreams.NewURLDataSource("keystone catalog", productStreamsURL, verify) 750 e.imageSources = append(e.imageSources, source) 751 } 752 return e.imageSources, nil 753 } 754 755 // GetToolsSources returns a list of sources which are used to search for simplestreams tools metadata. 756 func (e *environ) GetToolsSources() ([]simplestreams.DataSource, error) { 757 e.toolsBaseMutex.Lock() 758 defer e.toolsBaseMutex.Unlock() 759 760 if e.toolsSources != nil { 761 return e.toolsSources, nil 762 } 763 if !e.client.IsAuthenticated() { 764 err := e.client.Authenticate() 765 if err != nil { 766 return nil, err 767 } 768 } 769 verify := utils.VerifySSLHostnames 770 if !e.Config().SSLHostnameVerification() { 771 verify = utils.NoVerifySSLHostnames 772 } 773 // Add the simplestreams source off the control bucket. 774 e.toolsSources = append(e.toolsSources, storage.NewStorageSimpleStreamsDataSource( 775 "cloud storage", e.Storage(), storage.BaseToolsPath)) 776 // Add the simplestreams base URL from keystone if it is defined. 777 toolsURL, err := e.client.MakeServiceURL("juju-tools", nil) 778 if err == nil { 779 source := simplestreams.NewURLDataSource("keystone catalog", toolsURL, verify) 780 e.toolsSources = append(e.toolsSources, source) 781 } 782 return e.toolsSources, nil 783 } 784 785 // TODO(gz): Move this somewhere more reusable 786 const uuidPattern = "^([a-fA-F0-9]{8})-([a-fA-f0-9]{4})-([1-5][a-fA-f0-9]{3})-([a-fA-f0-9]{4})-([a-fA-f0-9]{12})$" 787 788 var uuidRegexp = regexp.MustCompile(uuidPattern) 789 790 // resolveNetwork takes either a network id or label and returns a network id 791 func (e *environ) resolveNetwork(networkName string) (string, error) { 792 if uuidRegexp.MatchString(networkName) { 793 // Network id supplied, assume valid as boot will fail if not 794 return networkName, nil 795 } 796 // Network label supplied, resolve to a network id 797 networks, err := e.nova().ListNetworks() 798 if err != nil { 799 return "", err 800 } 801 var networkIds = []string{} 802 for _, network := range networks { 803 if network.Label == networkName { 804 networkIds = append(networkIds, network.Id) 805 } 806 } 807 switch len(networkIds) { 808 case 1: 809 return networkIds[0], nil 810 case 0: 811 return "", fmt.Errorf("No networks exist with label %q", networkName) 812 } 813 return "", fmt.Errorf("Multiple networks with label %q: %v", networkName, networkIds) 814 } 815 816 // allocatePublicIP tries to find an available floating IP address, or 817 // allocates a new one, returning it, or an error 818 func (e *environ) allocatePublicIP() (*nova.FloatingIP, error) { 819 fips, err := e.nova().ListFloatingIPs() 820 if err != nil { 821 return nil, err 822 } 823 var newfip *nova.FloatingIP 824 for _, fip := range fips { 825 newfip = &fip 826 if fip.InstanceId != nil && *fip.InstanceId != "" { 827 // unavailable, skip 828 newfip = nil 829 continue 830 } else { 831 // unassigned, we can use it 832 return newfip, nil 833 } 834 } 835 if newfip == nil { 836 // allocate a new IP and use it 837 newfip, err = e.nova().AllocateFloatingIP() 838 if err != nil { 839 return nil, err 840 } 841 } 842 return newfip, nil 843 } 844 845 // assignPublicIP tries to assign the given floating IP address to the 846 // specified server, or returns an error. 847 func (e *environ) assignPublicIP(fip *nova.FloatingIP, serverId string) (err error) { 848 if fip == nil { 849 return fmt.Errorf("cannot assign a nil public IP to %q", serverId) 850 } 851 if fip.InstanceId != nil && *fip.InstanceId == serverId { 852 // IP already assigned, nothing to do 853 return nil 854 } 855 // At startup nw_info is not yet cached so this may fail 856 // temporarily while the server is being built 857 for a := common.LongAttempt.Start(); a.Next(); { 858 err = e.nova().AddServerFloatingIP(serverId, fip.IP) 859 if err == nil { 860 return nil 861 } 862 } 863 return err 864 } 865 866 // DistributeInstances implements the state.InstanceDistributor policy. 867 func (e *environ) DistributeInstances(candidates, distributionGroup []instance.Id) ([]instance.Id, error) { 868 return common.DistributeInstances(e, candidates, distributionGroup) 869 } 870 871 var bestAvailabilityZoneAllocations = common.BestAvailabilityZoneAllocations 872 873 // StartInstance is specified in the InstanceBroker interface. 874 func (e *environ) StartInstance(args environs.StartInstanceParams) (instance.Instance, *instance.HardwareCharacteristics, []network.Info, error) { 875 var availabilityZone string 876 if args.Placement != "" { 877 placement, err := e.parsePlacement(args.Placement) 878 if err != nil { 879 return nil, nil, nil, err 880 } 881 if !placement.availabilityZone.State.Available { 882 return nil, nil, nil, fmt.Errorf("availability zone %q is unavailable", placement.availabilityZone.Name) 883 } 884 availabilityZone = placement.availabilityZone.Name 885 } 886 887 // If no availability zone is specified, then automatically spread across 888 // the known zones for optimal spread across the instance distribution 889 // group. 890 if availabilityZone == "" { 891 var group []instance.Id 892 var err error 893 if args.DistributionGroup != nil { 894 group, err = args.DistributionGroup() 895 if err != nil { 896 return nil, nil, nil, err 897 } 898 } 899 bestAvailabilityZones, err := bestAvailabilityZoneAllocations(e, group) 900 if err != nil { 901 return nil, nil, nil, err 902 } 903 for availabilityZone = range bestAvailabilityZones { 904 break 905 } 906 } 907 908 if args.MachineConfig.HasNetworks() { 909 return nil, nil, nil, fmt.Errorf("starting instances with networks is not supported yet.") 910 } 911 912 series := args.Tools.OneSeries() 913 arches := args.Tools.Arches() 914 spec, err := findInstanceSpec(e, &instances.InstanceConstraint{ 915 Region: e.ecfg().region(), 916 Series: series, 917 Arches: arches, 918 Constraints: args.Constraints, 919 }) 920 if err != nil { 921 return nil, nil, nil, err 922 } 923 tools, err := args.Tools.Match(tools.Filter{Arch: spec.Image.Arch}) 924 if err != nil { 925 return nil, nil, nil, fmt.Errorf("chosen architecture %v not present in %v", spec.Image.Arch, arches) 926 } 927 928 args.MachineConfig.Tools = tools[0] 929 930 if err := environs.FinishMachineConfig(args.MachineConfig, e.Config(), args.Constraints); err != nil { 931 return nil, nil, nil, err 932 } 933 userData, err := environs.ComposeUserData(args.MachineConfig, nil) 934 if err != nil { 935 return nil, nil, nil, fmt.Errorf("cannot make user data: %v", err) 936 } 937 logger.Debugf("openstack user data; %d bytes", len(userData)) 938 var networks = []nova.ServerNetworks{} 939 usingNetwork := e.ecfg().network() 940 if usingNetwork != "" { 941 networkId, err := e.resolveNetwork(usingNetwork) 942 if err != nil { 943 return nil, nil, nil, err 944 } 945 logger.Debugf("using network id %q", networkId) 946 networks = append(networks, nova.ServerNetworks{NetworkId: networkId}) 947 } 948 withPublicIP := e.ecfg().useFloatingIP() 949 var publicIP *nova.FloatingIP 950 if withPublicIP { 951 if fip, err := e.allocatePublicIP(); err != nil { 952 return nil, nil, nil, fmt.Errorf("cannot allocate a public IP as needed: %v", err) 953 } else { 954 publicIP = fip 955 logger.Infof("allocated public IP %s", publicIP.IP) 956 } 957 } 958 cfg := e.Config() 959 groups, err := e.setUpGroups(args.MachineConfig.MachineId, cfg.StatePort(), cfg.APIPort()) 960 if err != nil { 961 return nil, nil, nil, fmt.Errorf("cannot set up groups: %v", err) 962 } 963 var groupNames = make([]nova.SecurityGroupName, len(groups)) 964 for i, g := range groups { 965 groupNames[i] = nova.SecurityGroupName{g.Name} 966 } 967 var opts = nova.RunServerOpts{ 968 Name: e.machineFullName(args.MachineConfig.MachineId), 969 FlavorId: spec.InstanceType.Id, 970 ImageId: spec.Image.Id, 971 UserData: userData, 972 SecurityGroupNames: groupNames, 973 Networks: networks, 974 AvailabilityZone: availabilityZone, 975 } 976 var server *nova.Entity 977 for a := shortAttempt.Start(); a.Next(); { 978 server, err = e.nova().RunServer(opts) 979 if err == nil || !gooseerrors.IsNotFound(err) { 980 break 981 } 982 } 983 if err != nil { 984 return nil, nil, nil, fmt.Errorf("cannot run instance: %v", err) 985 } 986 detail, err := e.nova().GetServer(server.Id) 987 if err != nil { 988 return nil, nil, nil, fmt.Errorf("cannot get started instance: %v", err) 989 } 990 inst := &openstackInstance{ 991 e: e, 992 serverDetail: detail, 993 arch: &spec.Image.Arch, 994 instType: &spec.InstanceType, 995 } 996 logger.Infof("started instance %q", inst.Id()) 997 if withPublicIP { 998 if err := e.assignPublicIP(publicIP, string(inst.Id())); err != nil { 999 if err := e.terminateInstances([]instance.Id{inst.Id()}); err != nil { 1000 // ignore the failure at this stage, just log it 1001 logger.Debugf("failed to terminate instance %q: %v", inst.Id(), err) 1002 } 1003 return nil, nil, nil, fmt.Errorf("cannot assign public address %s to instance %q: %v", publicIP.IP, inst.Id(), err) 1004 } 1005 logger.Infof("assigned public IP %s to %q", publicIP.IP, inst.Id()) 1006 } 1007 return inst, inst.hardwareCharacteristics(), nil, nil 1008 } 1009 1010 func (e *environ) StopInstances(ids ...instance.Id) error { 1011 // If in instance firewall mode, gather the security group names. 1012 var securityGroupNames []string 1013 if e.Config().FirewallMode() == config.FwInstance { 1014 instances, err := e.Instances(ids) 1015 if err == environs.ErrNoInstances { 1016 return nil 1017 } 1018 securityGroupNames = make([]string, 0, len(ids)) 1019 for _, inst := range instances { 1020 if inst == nil { 1021 continue 1022 } 1023 openstackName := inst.(*openstackInstance).getServerDetail().Name 1024 lastDashPos := strings.LastIndex(openstackName, "-") 1025 if lastDashPos == -1 { 1026 return fmt.Errorf("cannot identify machine ID in openstack server name %q", openstackName) 1027 } 1028 securityGroupName := e.machineGroupName(openstackName[lastDashPos+1:]) 1029 securityGroupNames = append(securityGroupNames, securityGroupName) 1030 } 1031 } 1032 logger.Debugf("terminating instances %v", ids) 1033 if err := e.terminateInstances(ids); err != nil { 1034 return err 1035 } 1036 if securityGroupNames != nil { 1037 return e.deleteSecurityGroups(securityGroupNames) 1038 } 1039 return nil 1040 } 1041 1042 // collectInstances tries to get information on each instance id in ids. 1043 // It fills the slots in the given map for known servers with status 1044 // either ACTIVE or BUILD. Returns a list of missing ids. 1045 func (e *environ) collectInstances(ids []instance.Id, out map[instance.Id]instance.Instance) []instance.Id { 1046 var err error 1047 serversById := make(map[string]nova.ServerDetail) 1048 if len(ids) == 1 { 1049 // most common case - single instance 1050 var server *nova.ServerDetail 1051 server, err = e.nova().GetServer(string(ids[0])) 1052 if server != nil { 1053 serversById[server.Id] = *server 1054 } 1055 } else { 1056 var servers []nova.ServerDetail 1057 servers, err = e.nova().ListServersDetail(e.machinesFilter()) 1058 for _, server := range servers { 1059 serversById[server.Id] = server 1060 } 1061 } 1062 if err != nil { 1063 return ids 1064 } 1065 var missing []instance.Id 1066 for _, id := range ids { 1067 if server, found := serversById[string(id)]; found { 1068 // HPCloud uses "BUILD(spawning)" as an intermediate BUILD states once networking is available. 1069 switch server.Status { 1070 case nova.StatusActive, nova.StatusBuild, nova.StatusBuildSpawning: 1071 // TODO(wallyworld): lookup the flavor details to fill in the instance type data 1072 out[id] = &openstackInstance{e: e, serverDetail: &server} 1073 continue 1074 } 1075 } 1076 missing = append(missing, id) 1077 } 1078 return missing 1079 } 1080 1081 func (e *environ) Instances(ids []instance.Id) ([]instance.Instance, error) { 1082 if len(ids) == 0 { 1083 return nil, nil 1084 } 1085 missing := ids 1086 found := make(map[instance.Id]instance.Instance) 1087 // Make a series of requests to cope with eventual consistency. 1088 // Each request will attempt to add more instances to the requested 1089 // set. 1090 for a := shortAttempt.Start(); a.Next(); { 1091 if missing = e.collectInstances(missing, found); len(missing) == 0 { 1092 break 1093 } 1094 } 1095 if len(found) == 0 { 1096 return nil, environs.ErrNoInstances 1097 } 1098 insts := make([]instance.Instance, len(ids)) 1099 var err error 1100 for i, id := range ids { 1101 if inst := found[id]; inst != nil { 1102 insts[i] = inst 1103 } else { 1104 err = environs.ErrPartialInstances 1105 } 1106 } 1107 return insts, err 1108 } 1109 1110 // AllocateAddress requests a new address to be allocated for the 1111 // given instance on the given network. This is not implemented on the 1112 // OpenStack provider yet. 1113 func (*environ) AllocateAddress(_ instance.Id, _ network.Id) (network.Address, error) { 1114 return network.Address{}, jujuerrors.NotImplementedf("AllocateAddress") 1115 } 1116 1117 // ListNetworks returns basic information about all networks known 1118 // by the provider for the environment. They may be unknown to juju 1119 // yet (i.e. when called initially or when a new network was created). 1120 // This is not implemented by the OpenStack provider yet. 1121 func (*environ) ListNetworks() ([]network.BasicInfo, error) { 1122 return nil, jujuerrors.NotImplementedf("ListNetworks") 1123 } 1124 1125 func (e *environ) AllInstances() (insts []instance.Instance, err error) { 1126 servers, err := e.nova().ListServersDetail(e.machinesFilter()) 1127 if err != nil { 1128 return nil, err 1129 } 1130 for _, server := range servers { 1131 if server.Status == nova.StatusActive || server.Status == nova.StatusBuild { 1132 var s = server 1133 // TODO(wallyworld): lookup the flavor details to fill in the instance type data 1134 insts = append(insts, &openstackInstance{ 1135 e: e, 1136 serverDetail: &s, 1137 }) 1138 } 1139 } 1140 return insts, err 1141 } 1142 1143 func (e *environ) Destroy() error { 1144 err := common.Destroy(e) 1145 if err != nil { 1146 return err 1147 } 1148 novaClient := e.nova() 1149 securityGroups, err := novaClient.ListSecurityGroups() 1150 if err != nil { 1151 return err 1152 } 1153 re, err := regexp.Compile(fmt.Sprintf("^%s(-\\d+)?$", e.jujuGroupName())) 1154 if err != nil { 1155 return err 1156 } 1157 globalGroupName := e.globalGroupName() 1158 for _, group := range securityGroups { 1159 if re.MatchString(group.Name) || group.Name == globalGroupName { 1160 err = novaClient.DeleteSecurityGroup(group.Id) 1161 if err != nil { 1162 logger.Warningf("cannot delete security group %q. Used by another environment?", group.Name) 1163 } 1164 } 1165 } 1166 return nil 1167 } 1168 1169 func (e *environ) globalGroupName() string { 1170 return fmt.Sprintf("%s-global", e.jujuGroupName()) 1171 } 1172 1173 func (e *environ) machineGroupName(machineId string) string { 1174 return fmt.Sprintf("%s-%s", e.jujuGroupName(), machineId) 1175 } 1176 1177 func (e *environ) jujuGroupName() string { 1178 return fmt.Sprintf("juju-%s", e.name) 1179 } 1180 1181 func (e *environ) machineFullName(machineId string) string { 1182 return fmt.Sprintf("juju-%s-%s", e.Name(), names.NewMachineTag(machineId)) 1183 } 1184 1185 // machinesFilter returns a nova.Filter matching all machines in the environment. 1186 func (e *environ) machinesFilter() *nova.Filter { 1187 filter := nova.NewFilter() 1188 filter.Set(nova.FilterServer, fmt.Sprintf("juju-%s-machine-\\d*", e.Name())) 1189 return filter 1190 } 1191 1192 func (e *environ) openPortsInGroup(name string, ports []network.Port) error { 1193 novaclient := e.nova() 1194 group, err := novaclient.SecurityGroupByName(name) 1195 if err != nil { 1196 return err 1197 } 1198 for _, port := range ports { 1199 _, err := novaclient.CreateSecurityGroupRule(nova.RuleInfo{ 1200 ParentGroupId: group.Id, 1201 FromPort: port.Number, 1202 ToPort: port.Number, 1203 IPProtocol: port.Protocol, 1204 Cidr: "0.0.0.0/0", 1205 }) 1206 if err != nil { 1207 // TODO: if err is not rule already exists, raise? 1208 logger.Debugf("error creating security group rule: %v", err.Error()) 1209 } 1210 } 1211 return nil 1212 } 1213 1214 func (e *environ) closePortsInGroup(name string, ports []network.Port) error { 1215 if len(ports) == 0 { 1216 return nil 1217 } 1218 novaclient := e.nova() 1219 group, err := novaclient.SecurityGroupByName(name) 1220 if err != nil { 1221 return err 1222 } 1223 // TODO: Hey look ma, it's quadratic 1224 for _, port := range ports { 1225 for _, p := range (*group).Rules { 1226 if p.IPProtocol == nil || *p.IPProtocol != port.Protocol || 1227 p.FromPort == nil || *p.FromPort != port.Number || 1228 p.ToPort == nil || *p.ToPort != port.Number { 1229 continue 1230 } 1231 err := novaclient.DeleteSecurityGroupRule(p.Id) 1232 if err != nil { 1233 return err 1234 } 1235 break 1236 } 1237 } 1238 return nil 1239 } 1240 1241 func (e *environ) portsInGroup(name string) (ports []network.Port, err error) { 1242 group, err := e.nova().SecurityGroupByName(name) 1243 if err != nil { 1244 return nil, err 1245 } 1246 for _, p := range (*group).Rules { 1247 for i := *p.FromPort; i <= *p.ToPort; i++ { 1248 ports = append(ports, network.Port{ 1249 Protocol: *p.IPProtocol, 1250 Number: i, 1251 }) 1252 } 1253 } 1254 network.SortPorts(ports) 1255 return ports, nil 1256 } 1257 1258 // TODO: following 30 lines nearly verbatim from environs/ec2 1259 1260 func (e *environ) OpenPorts(ports []network.Port) error { 1261 if e.Config().FirewallMode() != config.FwGlobal { 1262 return fmt.Errorf("invalid firewall mode %q for opening ports on environment", 1263 e.Config().FirewallMode()) 1264 } 1265 if err := e.openPortsInGroup(e.globalGroupName(), ports); err != nil { 1266 return err 1267 } 1268 logger.Infof("opened ports in global group: %v", ports) 1269 return nil 1270 } 1271 1272 func (e *environ) ClosePorts(ports []network.Port) error { 1273 if e.Config().FirewallMode() != config.FwGlobal { 1274 return fmt.Errorf("invalid firewall mode %q for closing ports on environment", 1275 e.Config().FirewallMode()) 1276 } 1277 if err := e.closePortsInGroup(e.globalGroupName(), ports); err != nil { 1278 return err 1279 } 1280 logger.Infof("closed ports in global group: %v", ports) 1281 return nil 1282 } 1283 1284 func (e *environ) Ports() ([]network.Port, error) { 1285 if e.Config().FirewallMode() != config.FwGlobal { 1286 return nil, fmt.Errorf("invalid firewall mode %q for retrieving ports from environment", 1287 e.Config().FirewallMode()) 1288 } 1289 return e.portsInGroup(e.globalGroupName()) 1290 } 1291 1292 func (e *environ) Provider() environs.EnvironProvider { 1293 return &providerInstance 1294 } 1295 1296 func (e *environ) setUpGlobalGroup(groupName string, statePort, apiPort int) (nova.SecurityGroup, error) { 1297 return e.ensureGroup(groupName, 1298 []nova.RuleInfo{ 1299 { 1300 IPProtocol: "tcp", 1301 FromPort: 22, 1302 ToPort: 22, 1303 Cidr: "0.0.0.0/0", 1304 }, 1305 { 1306 IPProtocol: "tcp", 1307 FromPort: statePort, 1308 ToPort: statePort, 1309 Cidr: "0.0.0.0/0", 1310 }, 1311 { 1312 IPProtocol: "tcp", 1313 FromPort: apiPort, 1314 ToPort: apiPort, 1315 Cidr: "0.0.0.0/0", 1316 }, 1317 { 1318 IPProtocol: "tcp", 1319 FromPort: 1, 1320 ToPort: 65535, 1321 }, 1322 { 1323 IPProtocol: "udp", 1324 FromPort: 1, 1325 ToPort: 65535, 1326 }, 1327 { 1328 IPProtocol: "icmp", 1329 FromPort: -1, 1330 ToPort: -1, 1331 }, 1332 }) 1333 } 1334 1335 // setUpGroups creates the security groups for the new machine, and 1336 // returns them. 1337 // 1338 // Instances are tagged with a group so they can be distinguished from 1339 // other instances that might be running on the same OpenStack account. 1340 // In addition, a specific machine security group is created for each 1341 // machine, so that its firewall rules can be configured per machine. 1342 // 1343 // Note: ideally we'd have a better way to determine group membership so that 2 1344 // people that happen to share an openstack account and name their environment 1345 // "openstack" don't end up destroying each other's machines. 1346 func (e *environ) setUpGroups(machineId string, statePort, apiPort int) ([]nova.SecurityGroup, error) { 1347 jujuGroup, err := e.setUpGlobalGroup(e.jujuGroupName(), statePort, apiPort) 1348 if err != nil { 1349 return nil, err 1350 } 1351 var machineGroup nova.SecurityGroup 1352 switch e.Config().FirewallMode() { 1353 case config.FwInstance: 1354 machineGroup, err = e.ensureGroup(e.machineGroupName(machineId), nil) 1355 case config.FwGlobal: 1356 machineGroup, err = e.ensureGroup(e.globalGroupName(), nil) 1357 } 1358 if err != nil { 1359 return nil, err 1360 } 1361 groups := []nova.SecurityGroup{jujuGroup, machineGroup} 1362 if e.ecfg().useDefaultSecurityGroup() { 1363 defaultGroup, err := e.nova().SecurityGroupByName("default") 1364 if err != nil { 1365 return nil, fmt.Errorf("loading default security group: %v", err) 1366 } 1367 groups = append(groups, *defaultGroup) 1368 } 1369 return groups, nil 1370 } 1371 1372 // zeroGroup holds the zero security group. 1373 var zeroGroup nova.SecurityGroup 1374 1375 // ensureGroup returns the security group with name and perms. 1376 // If a group with name does not exist, one will be created. 1377 // If it exists, its permissions are set to perms. 1378 func (e *environ) ensureGroup(name string, rules []nova.RuleInfo) (nova.SecurityGroup, error) { 1379 novaClient := e.nova() 1380 // First attempt to look up an existing group by name. 1381 group, err := novaClient.SecurityGroupByName(name) 1382 if err == nil { 1383 // Group exists, so assume it is correctly set up and return it. 1384 // TODO(jam): 2013-09-18 http://pad.lv/121795 1385 // We really should verify the group is set up correctly, 1386 // because deleting and re-creating environments can get us bad 1387 // groups (especially if they were set up under Python) 1388 return *group, nil 1389 } 1390 // Doesn't exist, so try and create it. 1391 group, err = novaClient.CreateSecurityGroup(name, "juju group") 1392 if err != nil { 1393 if !gooseerrors.IsDuplicateValue(err) { 1394 return zeroGroup, err 1395 } else { 1396 // We just tried to create a duplicate group, so load the existing group. 1397 group, err = novaClient.SecurityGroupByName(name) 1398 if err != nil { 1399 return zeroGroup, err 1400 } 1401 return *group, nil 1402 } 1403 } 1404 // The new group is created so now add the rules. 1405 group.Rules = make([]nova.SecurityGroupRule, len(rules)) 1406 for i, rule := range rules { 1407 rule.ParentGroupId = group.Id 1408 if rule.Cidr == "" { 1409 // http://pad.lv/1226996 Rules that don't have a CIDR 1410 // are meant to apply only to this group. If you don't 1411 // supply CIDR or GroupId then openstack assumes you 1412 // mean CIDR=0.0.0.0/0 1413 rule.GroupId = &group.Id 1414 } 1415 groupRule, err := novaClient.CreateSecurityGroupRule(rule) 1416 if err != nil && !gooseerrors.IsDuplicateValue(err) { 1417 return zeroGroup, err 1418 } 1419 group.Rules[i] = *groupRule 1420 } 1421 return *group, nil 1422 } 1423 1424 // deleteSecurityGroups deletes the given security groups. If a security 1425 // group is also used by another environment (see bug #1300755), an attempt 1426 // to delete this group fails. A warning is logged in this case. 1427 func (e *environ) deleteSecurityGroups(securityGroupNames []string) error { 1428 novaclient := e.nova() 1429 allSecurityGroups, err := novaclient.ListSecurityGroups() 1430 if err != nil { 1431 return err 1432 } 1433 for _, securityGroup := range allSecurityGroups { 1434 for _, name := range securityGroupNames { 1435 if securityGroup.Name == name { 1436 err := novaclient.DeleteSecurityGroup(securityGroup.Id) 1437 if err != nil { 1438 logger.Warningf("cannot delete security group %q. Used by another environment?", name) 1439 } 1440 break 1441 } 1442 } 1443 } 1444 return nil 1445 } 1446 1447 func (e *environ) terminateInstances(ids []instance.Id) error { 1448 if len(ids) == 0 { 1449 return nil 1450 } 1451 var firstErr error 1452 novaClient := e.nova() 1453 for _, id := range ids { 1454 err := novaClient.DeleteServer(string(id)) 1455 if gooseerrors.IsNotFound(err) { 1456 err = nil 1457 } 1458 if err != nil && firstErr == nil { 1459 logger.Debugf("error terminating instance %q: %v", id, err) 1460 firstErr = err 1461 } 1462 } 1463 return firstErr 1464 } 1465 1466 // MetadataLookupParams returns parameters which are used to query simplestreams metadata. 1467 func (e *environ) MetadataLookupParams(region string) (*simplestreams.MetadataLookupParams, error) { 1468 if region == "" { 1469 region = e.ecfg().region() 1470 } 1471 cloudSpec, err := e.cloudSpec(region) 1472 if err != nil { 1473 return nil, err 1474 } 1475 return &simplestreams.MetadataLookupParams{ 1476 Series: config.PreferredSeries(e.ecfg()), 1477 Region: cloudSpec.Region, 1478 Endpoint: cloudSpec.Endpoint, 1479 Architectures: arch.AllSupportedArches, 1480 }, nil 1481 } 1482 1483 // Region is specified in the HasRegion interface. 1484 func (e *environ) Region() (simplestreams.CloudSpec, error) { 1485 return e.cloudSpec(e.ecfg().region()) 1486 } 1487 1488 func (e *environ) cloudSpec(region string) (simplestreams.CloudSpec, error) { 1489 return simplestreams.CloudSpec{ 1490 Region: region, 1491 Endpoint: e.ecfg().authURL(), 1492 }, nil 1493 }