github.com/cloudbase/juju-core@v0.0.0-20140504232958-a7271ac7912f/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/juju/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(ctx environs.BootstrapContext, 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", "arm64", "ppc64"}, 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 func (e *environ) Name() string { 487 return e.name 488 } 489 490 func (e *environ) Storage() storage.Storage { 491 e.ecfgMutex.Lock() 492 stor := e.storageUnlocked 493 e.ecfgMutex.Unlock() 494 return stor 495 } 496 497 func (e *environ) Bootstrap(ctx environs.BootstrapContext, cons constraints.Value) error { 498 // The client's authentication may have been reset when finding tools if the agent-version 499 // attribute was updated so we need to re-authenticate. This will be a no-op if already authenticated. 500 // An authenticated client is needed for the URL() call below. 501 err := e.client.Authenticate() 502 if err != nil { 503 return err 504 } 505 return common.Bootstrap(ctx, e, cons) 506 } 507 508 func (e *environ) StateInfo() (*state.Info, *api.Info, error) { 509 return common.StateInfo(e) 510 } 511 512 func (e *environ) Config() *config.Config { 513 return e.ecfg().Config 514 } 515 516 func (e *environ) authClient(ecfg *environConfig, authModeCfg AuthMode) client.AuthenticatingClient { 517 cred := &identity.Credentials{ 518 User: ecfg.username(), 519 Secrets: ecfg.password(), 520 Region: ecfg.region(), 521 TenantName: ecfg.tenantName(), 522 URL: ecfg.authURL(), 523 } 524 // authModeCfg has already been validated so we know it's one of the values below. 525 var authMode identity.AuthMode 526 switch authModeCfg { 527 case AuthLegacy: 528 authMode = identity.AuthLegacy 529 case AuthUserPass: 530 authMode = identity.AuthUserPass 531 case AuthKeyPair: 532 authMode = identity.AuthKeyPair 533 cred.User = ecfg.accessKey() 534 cred.Secrets = ecfg.secretKey() 535 } 536 newClient := client.NewClient 537 if !ecfg.SSLHostnameVerification() { 538 newClient = client.NewNonValidatingClient 539 } 540 return newClient(cred, authMode, nil) 541 } 542 543 func (e *environ) SetConfig(cfg *config.Config) error { 544 ecfg, err := providerInstance.newConfig(cfg) 545 if err != nil { 546 return err 547 } 548 // At this point, the authentication method config value has been validated so we extract it's value here 549 // to avoid having to validate again each time when creating the OpenStack client. 550 var authModeCfg AuthMode 551 e.ecfgMutex.Lock() 552 defer e.ecfgMutex.Unlock() 553 authModeCfg = AuthMode(ecfg.authMode()) 554 e.ecfgUnlocked = ecfg 555 556 e.client = e.authClient(ecfg, authModeCfg) 557 e.novaUnlocked = nova.New(e.client) 558 559 // create new control storage instance, existing instances continue 560 // to reference their existing configuration. 561 // public storage instance creation is deferred until needed since authenticated 562 // access to the identity service is required so that any juju-tools endpoint can be used. 563 e.storageUnlocked = &openstackstorage{ 564 containerName: ecfg.controlBucket(), 565 // this is possibly just a hack - if the ACL is swift.Private, 566 // the machine won't be able to get the tools (401 error) 567 containerACL: swift.PublicRead, 568 swift: swift.New(e.client)} 569 return nil 570 } 571 572 // GetImageSources returns a list of sources which are used to search for simplestreams image metadata. 573 func (e *environ) GetImageSources() ([]simplestreams.DataSource, error) { 574 e.imageBaseMutex.Lock() 575 defer e.imageBaseMutex.Unlock() 576 577 if e.imageSources != nil { 578 return e.imageSources, nil 579 } 580 if !e.client.IsAuthenticated() { 581 err := e.client.Authenticate() 582 if err != nil { 583 return nil, err 584 } 585 } 586 // Add the simplestreams source off the control bucket. 587 e.imageSources = append(e.imageSources, storage.NewStorageSimpleStreamsDataSource( 588 "cloud storage", e.Storage(), storage.BaseImagesPath)) 589 // Add the simplestreams base URL from keystone if it is defined. 590 productStreamsURL, err := e.client.MakeServiceURL("product-streams", nil) 591 if err == nil { 592 verify := simplestreams.VerifySSLHostnames 593 if !e.Config().SSLHostnameVerification() { 594 verify = simplestreams.NoVerifySSLHostnames 595 } 596 source := simplestreams.NewURLDataSource("keystone catalog", productStreamsURL, verify) 597 e.imageSources = append(e.imageSources, source) 598 } 599 return e.imageSources, nil 600 } 601 602 // GetToolsSources returns a list of sources which are used to search for simplestreams tools metadata. 603 func (e *environ) GetToolsSources() ([]simplestreams.DataSource, error) { 604 e.toolsBaseMutex.Lock() 605 defer e.toolsBaseMutex.Unlock() 606 607 if e.toolsSources != nil { 608 return e.toolsSources, nil 609 } 610 if !e.client.IsAuthenticated() { 611 err := e.client.Authenticate() 612 if err != nil { 613 return nil, err 614 } 615 } 616 verify := simplestreams.VerifySSLHostnames 617 if !e.Config().SSLHostnameVerification() { 618 verify = simplestreams.NoVerifySSLHostnames 619 } 620 // Add the simplestreams source off the control bucket. 621 e.toolsSources = append(e.toolsSources, storage.NewStorageSimpleStreamsDataSource( 622 "cloud storage", e.Storage(), storage.BaseToolsPath)) 623 // Add the simplestreams base URL from keystone if it is defined. 624 toolsURL, err := e.client.MakeServiceURL("juju-tools", nil) 625 if err == nil { 626 source := simplestreams.NewURLDataSource("keystone catalog", toolsURL, verify) 627 e.toolsSources = append(e.toolsSources, source) 628 } 629 return e.toolsSources, nil 630 } 631 632 // TODO(gz): Move this somewhere more reusable 633 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})$" 634 635 var uuidRegexp = regexp.MustCompile(uuidPattern) 636 637 // resolveNetwork takes either a network id or label and returns a network id 638 func (e *environ) resolveNetwork(networkName string) (string, error) { 639 if uuidRegexp.MatchString(networkName) { 640 // Network id supplied, assume valid as boot will fail if not 641 return networkName, nil 642 } 643 // Network label supplied, resolve to a network id 644 networks, err := e.nova().ListNetworks() 645 if err != nil { 646 return "", err 647 } 648 var networkIds = []string{} 649 for _, network := range networks { 650 if network.Label == networkName { 651 networkIds = append(networkIds, network.Id) 652 } 653 } 654 switch len(networkIds) { 655 case 1: 656 return networkIds[0], nil 657 case 0: 658 return "", fmt.Errorf("No networks exist with label %q", networkName) 659 } 660 return "", fmt.Errorf("Multiple networks with label %q: %v", networkName, networkIds) 661 } 662 663 // allocatePublicIP tries to find an available floating IP address, or 664 // allocates a new one, returning it, or an error 665 func (e *environ) allocatePublicIP() (*nova.FloatingIP, error) { 666 fips, err := e.nova().ListFloatingIPs() 667 if err != nil { 668 return nil, err 669 } 670 var newfip *nova.FloatingIP 671 for _, fip := range fips { 672 newfip = &fip 673 if fip.InstanceId != nil && *fip.InstanceId != "" { 674 // unavailable, skip 675 newfip = nil 676 continue 677 } else { 678 // unassigned, we can use it 679 return newfip, nil 680 } 681 } 682 if newfip == nil { 683 // allocate a new IP and use it 684 newfip, err = e.nova().AllocateFloatingIP() 685 if err != nil { 686 return nil, err 687 } 688 } 689 return newfip, nil 690 } 691 692 // assignPublicIP tries to assign the given floating IP address to the 693 // specified server, or returns an error. 694 func (e *environ) assignPublicIP(fip *nova.FloatingIP, serverId string) (err error) { 695 if fip == nil { 696 return fmt.Errorf("cannot assign a nil public IP to %q", serverId) 697 } 698 if fip.InstanceId != nil && *fip.InstanceId == serverId { 699 // IP already assigned, nothing to do 700 return nil 701 } 702 // At startup nw_info is not yet cached so this may fail 703 // temporarily while the server is being built 704 for a := common.LongAttempt.Start(); a.Next(); { 705 err = e.nova().AddServerFloatingIP(serverId, fip.IP) 706 if err == nil { 707 return nil 708 } 709 } 710 return err 711 } 712 713 // StartInstance is specified in the InstanceBroker interface. 714 func (e *environ) StartInstance(cons constraints.Value, possibleTools tools.List, 715 machineConfig *cloudinit.MachineConfig) (instance.Instance, *instance.HardwareCharacteristics, error) { 716 717 series := possibleTools.OneSeries() 718 arches := possibleTools.Arches() 719 spec, err := findInstanceSpec(e, &instances.InstanceConstraint{ 720 Region: e.ecfg().region(), 721 Series: series, 722 Arches: arches, 723 Constraints: cons, 724 }) 725 if err != nil { 726 return nil, nil, err 727 } 728 tools, err := possibleTools.Match(tools.Filter{Arch: spec.Image.Arch}) 729 if err != nil { 730 return nil, nil, fmt.Errorf("chosen architecture %v not present in %v", spec.Image.Arch, arches) 731 } 732 733 machineConfig.Tools = tools[0] 734 735 if err := environs.FinishMachineConfig(machineConfig, e.Config(), cons); err != nil { 736 return nil, nil, err 737 } 738 userData, err := environs.ComposeUserData(machineConfig) 739 if err != nil { 740 return nil, nil, fmt.Errorf("cannot make user data: %v", err) 741 } 742 logger.Debugf("openstack user data; %d bytes", len(userData)) 743 var networks = []nova.ServerNetworks{} 744 usingNetwork := e.ecfg().network() 745 if usingNetwork != "" { 746 networkId, err := e.resolveNetwork(usingNetwork) 747 if err != nil { 748 return nil, nil, err 749 } 750 logger.Debugf("using network id %q", networkId) 751 networks = append(networks, nova.ServerNetworks{NetworkId: networkId}) 752 } 753 withPublicIP := e.ecfg().useFloatingIP() 754 var publicIP *nova.FloatingIP 755 if withPublicIP { 756 if fip, err := e.allocatePublicIP(); err != nil { 757 return nil, nil, fmt.Errorf("cannot allocate a public IP as needed: %v", err) 758 } else { 759 publicIP = fip 760 logger.Infof("allocated public IP %s", publicIP.IP) 761 } 762 } 763 cfg := e.Config() 764 groups, err := e.setUpGroups(machineConfig.MachineId, cfg.StatePort(), cfg.APIPort()) 765 if err != nil { 766 return nil, nil, fmt.Errorf("cannot set up groups: %v", err) 767 } 768 var groupNames = make([]nova.SecurityGroupName, len(groups)) 769 for i, g := range groups { 770 groupNames[i] = nova.SecurityGroupName{g.Name} 771 } 772 var opts = nova.RunServerOpts{ 773 Name: e.machineFullName(machineConfig.MachineId), 774 FlavorId: spec.InstanceType.Id, 775 ImageId: spec.Image.Id, 776 UserData: userData, 777 SecurityGroupNames: groupNames, 778 Networks: networks, 779 } 780 var server *nova.Entity 781 for a := shortAttempt.Start(); a.Next(); { 782 server, err = e.nova().RunServer(opts) 783 if err == nil || !gooseerrors.IsNotFound(err) { 784 break 785 } 786 } 787 if err != nil { 788 return nil, nil, fmt.Errorf("cannot run instance: %v", err) 789 } 790 detail, err := e.nova().GetServer(server.Id) 791 if err != nil { 792 return nil, nil, fmt.Errorf("cannot get started instance: %v", err) 793 } 794 inst := &openstackInstance{ 795 e: e, 796 serverDetail: detail, 797 arch: &spec.Image.Arch, 798 instType: &spec.InstanceType, 799 } 800 logger.Infof("started instance %q", inst.Id()) 801 if withPublicIP { 802 if err := e.assignPublicIP(publicIP, string(inst.Id())); err != nil { 803 if err := e.terminateInstances([]instance.Id{inst.Id()}); err != nil { 804 // ignore the failure at this stage, just log it 805 logger.Debugf("failed to terminate instance %q: %v", inst.Id(), err) 806 } 807 return nil, nil, fmt.Errorf("cannot assign public address %s to instance %q: %v", publicIP.IP, inst.Id(), err) 808 } 809 logger.Infof("assigned public IP %s to %q", publicIP.IP, inst.Id()) 810 } 811 return inst, inst.hardwareCharacteristics(), nil 812 } 813 814 func (e *environ) StopInstances(insts []instance.Instance) error { 815 ids := make([]instance.Id, len(insts)) 816 for i, inst := range insts { 817 instanceValue, ok := inst.(*openstackInstance) 818 if !ok { 819 return errors.New("Incompatible instance.Instance supplied") 820 } 821 ids[i] = instanceValue.Id() 822 } 823 logger.Debugf("terminating instances %v", ids) 824 return e.terminateInstances(ids) 825 } 826 827 // collectInstances tries to get information on each instance id in ids. 828 // It fills the slots in the given map for known servers with status 829 // either ACTIVE or BUILD. Returns a list of missing ids. 830 func (e *environ) collectInstances(ids []instance.Id, out map[instance.Id]instance.Instance) []instance.Id { 831 var err error 832 serversById := make(map[string]nova.ServerDetail) 833 if len(ids) == 1 { 834 // most common case - single instance 835 var server *nova.ServerDetail 836 server, err = e.nova().GetServer(string(ids[0])) 837 if server != nil { 838 serversById[server.Id] = *server 839 } 840 } else { 841 var servers []nova.ServerDetail 842 servers, err = e.nova().ListServersDetail(e.machinesFilter()) 843 for _, server := range servers { 844 serversById[server.Id] = server 845 } 846 } 847 if err != nil { 848 return ids 849 } 850 var missing []instance.Id 851 for _, id := range ids { 852 if server, found := serversById[string(id)]; found { 853 // HPCloud uses "BUILD(spawning)" as an intermediate BUILD states once networking is available. 854 switch server.Status { 855 case nova.StatusActive, nova.StatusBuild, nova.StatusBuildSpawning: 856 // TODO(wallyworld): lookup the flavor details to fill in the instance type data 857 out[id] = &openstackInstance{e: e, serverDetail: &server} 858 continue 859 } 860 } 861 missing = append(missing, id) 862 } 863 return missing 864 } 865 866 func (e *environ) Instances(ids []instance.Id) ([]instance.Instance, error) { 867 if len(ids) == 0 { 868 return nil, nil 869 } 870 missing := ids 871 found := make(map[instance.Id]instance.Instance) 872 // Make a series of requests to cope with eventual consistency. 873 // Each request will attempt to add more instances to the requested 874 // set. 875 for a := shortAttempt.Start(); a.Next(); { 876 if missing = e.collectInstances(missing, found); len(missing) == 0 { 877 break 878 } 879 } 880 if len(found) == 0 { 881 return nil, environs.ErrNoInstances 882 } 883 insts := make([]instance.Instance, len(ids)) 884 var err error 885 for i, id := range ids { 886 if inst := found[id]; inst != nil { 887 insts[i] = inst 888 } else { 889 err = environs.ErrPartialInstances 890 } 891 } 892 return insts, err 893 } 894 895 func (e *environ) AllInstances() (insts []instance.Instance, err error) { 896 servers, err := e.nova().ListServersDetail(e.machinesFilter()) 897 if err != nil { 898 return nil, err 899 } 900 for _, server := range servers { 901 if server.Status == nova.StatusActive || server.Status == nova.StatusBuild { 902 var s = server 903 // TODO(wallyworld): lookup the flavor details to fill in the instance type data 904 insts = append(insts, &openstackInstance{ 905 e: e, 906 serverDetail: &s, 907 }) 908 } 909 } 910 return insts, err 911 } 912 913 func (e *environ) Destroy() error { 914 return common.Destroy(e) 915 } 916 917 func (e *environ) globalGroupName() string { 918 return fmt.Sprintf("%s-global", e.jujuGroupName()) 919 } 920 921 func (e *environ) machineGroupName(machineId string) string { 922 return fmt.Sprintf("%s-%s", e.jujuGroupName(), machineId) 923 } 924 925 func (e *environ) jujuGroupName() string { 926 return fmt.Sprintf("juju-%s", e.name) 927 } 928 929 func (e *environ) machineFullName(machineId string) string { 930 return fmt.Sprintf("juju-%s-%s", e.Name(), names.MachineTag(machineId)) 931 } 932 933 // machinesFilter returns a nova.Filter matching all machines in the environment. 934 func (e *environ) machinesFilter() *nova.Filter { 935 filter := nova.NewFilter() 936 filter.Set(nova.FilterServer, fmt.Sprintf("juju-%s-machine-\\d*", e.Name())) 937 return filter 938 } 939 940 func (e *environ) openPortsInGroup(name string, ports []instance.Port) error { 941 novaclient := e.nova() 942 group, err := novaclient.SecurityGroupByName(name) 943 if err != nil { 944 return err 945 } 946 for _, port := range ports { 947 _, err := novaclient.CreateSecurityGroupRule(nova.RuleInfo{ 948 ParentGroupId: group.Id, 949 FromPort: port.Number, 950 ToPort: port.Number, 951 IPProtocol: port.Protocol, 952 Cidr: "0.0.0.0/0", 953 }) 954 if err != nil { 955 // TODO: if err is not rule already exists, raise? 956 logger.Debugf("error creating security group rule: %v", err.Error()) 957 } 958 } 959 return nil 960 } 961 962 func (e *environ) closePortsInGroup(name string, ports []instance.Port) error { 963 if len(ports) == 0 { 964 return nil 965 } 966 novaclient := e.nova() 967 group, err := novaclient.SecurityGroupByName(name) 968 if err != nil { 969 return err 970 } 971 // TODO: Hey look ma, it's quadratic 972 for _, port := range ports { 973 for _, p := range (*group).Rules { 974 if p.IPProtocol == nil || *p.IPProtocol != port.Protocol || 975 p.FromPort == nil || *p.FromPort != port.Number || 976 p.ToPort == nil || *p.ToPort != port.Number { 977 continue 978 } 979 err := novaclient.DeleteSecurityGroupRule(p.Id) 980 if err != nil { 981 return err 982 } 983 break 984 } 985 } 986 return nil 987 } 988 989 func (e *environ) portsInGroup(name string) (ports []instance.Port, err error) { 990 group, err := e.nova().SecurityGroupByName(name) 991 if err != nil { 992 return nil, err 993 } 994 for _, p := range (*group).Rules { 995 for i := *p.FromPort; i <= *p.ToPort; i++ { 996 ports = append(ports, instance.Port{ 997 Protocol: *p.IPProtocol, 998 Number: i, 999 }) 1000 } 1001 } 1002 instance.SortPorts(ports) 1003 return ports, nil 1004 } 1005 1006 // TODO: following 30 lines nearly verbatim from environs/ec2 1007 1008 func (e *environ) OpenPorts(ports []instance.Port) error { 1009 if e.Config().FirewallMode() != config.FwGlobal { 1010 return fmt.Errorf("invalid firewall mode %q for opening ports on environment", 1011 e.Config().FirewallMode()) 1012 } 1013 if err := e.openPortsInGroup(e.globalGroupName(), ports); err != nil { 1014 return err 1015 } 1016 logger.Infof("opened ports in global group: %v", ports) 1017 return nil 1018 } 1019 1020 func (e *environ) ClosePorts(ports []instance.Port) error { 1021 if e.Config().FirewallMode() != config.FwGlobal { 1022 return fmt.Errorf("invalid firewall mode %q for closing ports on environment", 1023 e.Config().FirewallMode()) 1024 } 1025 if err := e.closePortsInGroup(e.globalGroupName(), ports); err != nil { 1026 return err 1027 } 1028 logger.Infof("closed ports in global group: %v", ports) 1029 return nil 1030 } 1031 1032 func (e *environ) Ports() ([]instance.Port, error) { 1033 if e.Config().FirewallMode() != config.FwGlobal { 1034 return nil, fmt.Errorf("invalid firewall mode %q for retrieving ports from environment", 1035 e.Config().FirewallMode()) 1036 } 1037 return e.portsInGroup(e.globalGroupName()) 1038 } 1039 1040 func (e *environ) Provider() environs.EnvironProvider { 1041 return &providerInstance 1042 } 1043 1044 func (e *environ) setUpGlobalGroup(groupName string, statePort, apiPort int) (nova.SecurityGroup, error) { 1045 return e.ensureGroup(groupName, 1046 []nova.RuleInfo{ 1047 { 1048 IPProtocol: "tcp", 1049 FromPort: 22, 1050 ToPort: 22, 1051 Cidr: "0.0.0.0/0", 1052 }, 1053 { 1054 IPProtocol: "tcp", 1055 FromPort: statePort, 1056 ToPort: statePort, 1057 Cidr: "0.0.0.0/0", 1058 }, 1059 { 1060 IPProtocol: "tcp", 1061 FromPort: apiPort, 1062 ToPort: apiPort, 1063 Cidr: "0.0.0.0/0", 1064 }, 1065 { 1066 IPProtocol: "tcp", 1067 FromPort: 1, 1068 ToPort: 65535, 1069 }, 1070 { 1071 IPProtocol: "udp", 1072 FromPort: 1, 1073 ToPort: 65535, 1074 }, 1075 { 1076 IPProtocol: "icmp", 1077 FromPort: -1, 1078 ToPort: -1, 1079 }, 1080 }) 1081 } 1082 1083 // setUpGroups creates the security groups for the new machine, and 1084 // returns them. 1085 // 1086 // Instances are tagged with a group so they can be distinguished from 1087 // other instances that might be running on the same OpenStack account. 1088 // In addition, a specific machine security group is created for each 1089 // machine, so that its firewall rules can be configured per machine. 1090 // 1091 // Note: ideally we'd have a better way to determine group membership so that 2 1092 // people that happen to share an openstack account and name their environment 1093 // "openstack" don't end up destroying each other's machines. 1094 func (e *environ) setUpGroups(machineId string, statePort, apiPort int) ([]nova.SecurityGroup, error) { 1095 jujuGroup, err := e.setUpGlobalGroup(e.jujuGroupName(), statePort, apiPort) 1096 if err != nil { 1097 return nil, err 1098 } 1099 var machineGroup nova.SecurityGroup 1100 switch e.Config().FirewallMode() { 1101 case config.FwInstance: 1102 machineGroup, err = e.ensureGroup(e.machineGroupName(machineId), nil) 1103 case config.FwGlobal: 1104 machineGroup, err = e.ensureGroup(e.globalGroupName(), nil) 1105 } 1106 if err != nil { 1107 return nil, err 1108 } 1109 groups := []nova.SecurityGroup{jujuGroup, machineGroup} 1110 if e.ecfg().useDefaultSecurityGroup() { 1111 defaultGroup, err := e.nova().SecurityGroupByName("default") 1112 if err != nil { 1113 return nil, fmt.Errorf("loading default security group: %v", err) 1114 } 1115 groups = append(groups, *defaultGroup) 1116 } 1117 return groups, nil 1118 } 1119 1120 // zeroGroup holds the zero security group. 1121 var zeroGroup nova.SecurityGroup 1122 1123 // ensureGroup returns the security group with name and perms. 1124 // If a group with name does not exist, one will be created. 1125 // If it exists, its permissions are set to perms. 1126 func (e *environ) ensureGroup(name string, rules []nova.RuleInfo) (nova.SecurityGroup, error) { 1127 novaClient := e.nova() 1128 // First attempt to look up an existing group by name. 1129 group, err := novaClient.SecurityGroupByName(name) 1130 if err == nil { 1131 // Group exists, so assume it is correctly set up and return it. 1132 // TODO(jam): 2013-09-18 http://pad.lv/121795 1133 // We really should verify the group is set up correctly, 1134 // because deleting and re-creating environments can get us bad 1135 // groups (especially if they were set up under Python) 1136 return *group, nil 1137 } 1138 // Doesn't exist, so try and create it. 1139 group, err = novaClient.CreateSecurityGroup(name, "juju group") 1140 if err != nil { 1141 if !gooseerrors.IsDuplicateValue(err) { 1142 return zeroGroup, err 1143 } else { 1144 // We just tried to create a duplicate group, so load the existing group. 1145 group, err = novaClient.SecurityGroupByName(name) 1146 if err != nil { 1147 return zeroGroup, err 1148 } 1149 return *group, nil 1150 } 1151 } 1152 // The new group is created so now add the rules. 1153 group.Rules = make([]nova.SecurityGroupRule, len(rules)) 1154 for i, rule := range rules { 1155 rule.ParentGroupId = group.Id 1156 if rule.Cidr == "" { 1157 // http://pad.lv/1226996 Rules that don't have a CIDR 1158 // are meant to apply only to this group. If you don't 1159 // supply CIDR or GroupId then openstack assumes you 1160 // mean CIDR=0.0.0.0/0 1161 rule.GroupId = &group.Id 1162 } 1163 groupRule, err := novaClient.CreateSecurityGroupRule(rule) 1164 if err != nil && !gooseerrors.IsDuplicateValue(err) { 1165 return zeroGroup, err 1166 } 1167 group.Rules[i] = *groupRule 1168 } 1169 return *group, nil 1170 } 1171 1172 func (e *environ) terminateInstances(ids []instance.Id) error { 1173 if len(ids) == 0 { 1174 return nil 1175 } 1176 var firstErr error 1177 novaClient := e.nova() 1178 for _, id := range ids { 1179 err := novaClient.DeleteServer(string(id)) 1180 if gooseerrors.IsNotFound(err) { 1181 err = nil 1182 } 1183 if err != nil && firstErr == nil { 1184 logger.Debugf("error terminating instance %q: %v", id, err) 1185 firstErr = err 1186 } 1187 } 1188 return firstErr 1189 } 1190 1191 // MetadataLookupParams returns parameters which are used to query simplestreams metadata. 1192 func (e *environ) MetadataLookupParams(region string) (*simplestreams.MetadataLookupParams, error) { 1193 if region == "" { 1194 region = e.ecfg().region() 1195 } 1196 return &simplestreams.MetadataLookupParams{ 1197 Series: e.ecfg().DefaultSeries(), 1198 Region: region, 1199 Endpoint: e.ecfg().authURL(), 1200 Architectures: []string{"amd64", "arm", "arm64", "ppc64"}, 1201 }, nil 1202 } 1203 1204 // Region is specified in the HasRegion interface. 1205 func (e *environ) Region() (simplestreams.CloudSpec, error) { 1206 return simplestreams.CloudSpec{ 1207 Region: e.ecfg().region(), 1208 Endpoint: e.ecfg().authURL(), 1209 }, nil 1210 }