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