github.com/mattyw/juju@v0.0.0-20140610034352-732aecd63861/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/network" 33 "github.com/juju/juju/environs/simplestreams" 34 "github.com/juju/juju/environs/storage" 35 envtools "github.com/juju/juju/environs/tools" 36 "github.com/juju/juju/instance" 37 "github.com/juju/juju/juju/arch" 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: false 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. az-1.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 305 var _ environs.Environ = (*environ)(nil) 306 var _ imagemetadata.SupportsCustomSources = (*environ)(nil) 307 var _ envtools.SupportsCustomSources = (*environ)(nil) 308 var _ simplestreams.HasRegion = (*environ)(nil) 309 var _ state.Prechecker = (*environ)(nil) 310 311 type openstackInstance struct { 312 e *environ 313 instType *instances.InstanceType 314 arch *string 315 316 mu sync.Mutex 317 serverDetail *nova.ServerDetail 318 } 319 320 func (inst *openstackInstance) String() string { 321 return string(inst.Id()) 322 } 323 324 var _ instance.Instance = (*openstackInstance)(nil) 325 326 func (inst *openstackInstance) Refresh() error { 327 inst.mu.Lock() 328 defer inst.mu.Unlock() 329 server, err := inst.e.nova().GetServer(inst.serverDetail.Id) 330 if err != nil { 331 return err 332 } 333 inst.serverDetail = server 334 return nil 335 } 336 337 func (inst *openstackInstance) getServerDetail() *nova.ServerDetail { 338 inst.mu.Lock() 339 defer inst.mu.Unlock() 340 return inst.serverDetail 341 } 342 343 func (inst *openstackInstance) Id() instance.Id { 344 return instance.Id(inst.getServerDetail().Id) 345 } 346 347 func (inst *openstackInstance) Status() string { 348 return inst.getServerDetail().Status 349 } 350 351 func (inst *openstackInstance) hardwareCharacteristics() *instance.HardwareCharacteristics { 352 hc := &instance.HardwareCharacteristics{Arch: inst.arch} 353 if inst.instType != nil { 354 hc.Mem = &inst.instType.Mem 355 // openstack is special in that a 0-size root disk means that 356 // the root disk will result in an instance with a root disk 357 // the same size as the image that created it, so we just set 358 // the HardwareCharacteristics to nil to signal that we don't 359 // know what the correct size is. 360 if inst.instType.RootDisk == 0 { 361 hc.RootDisk = nil 362 } else { 363 hc.RootDisk = &inst.instType.RootDisk 364 } 365 hc.CpuCores = &inst.instType.CpuCores 366 hc.CpuPower = inst.instType.CpuPower 367 // tags not currently supported on openstack 368 } 369 return hc 370 } 371 372 // getAddresses returns the existing server information on addresses, 373 // but fetches the details over the api again if no addresses exist. 374 func (inst *openstackInstance) getAddresses() (map[string][]nova.IPAddress, error) { 375 addrs := inst.getServerDetail().Addresses 376 if len(addrs) == 0 { 377 server, err := inst.e.nova().GetServer(string(inst.Id())) 378 if err != nil { 379 return nil, err 380 } 381 addrs = server.Addresses 382 } 383 return addrs, nil 384 } 385 386 // Addresses implements instance.Addresses() returning generic address 387 // details for the instances, and calling the openstack api if needed. 388 func (inst *openstackInstance) Addresses() ([]instance.Address, error) { 389 addresses, err := inst.getAddresses() 390 if err != nil { 391 return nil, err 392 } 393 return convertNovaAddresses(addresses), nil 394 } 395 396 // convertNovaAddresses returns nova addresses in generic format 397 func convertNovaAddresses(addresses map[string][]nova.IPAddress) []instance.Address { 398 // TODO(gz) Network ordering may be significant but is not preserved by 399 // the map, see lp:1188126 for example. That could potentially be fixed 400 // in goose, or left to be derived by other means. 401 var machineAddresses []instance.Address 402 for network, ips := range addresses { 403 networkscope := instance.NetworkUnknown 404 // For canonistack and hpcloud, public floating addresses may 405 // be put in networks named something other than public. Rely 406 // on address sanity logic to catch and mark them corectly. 407 if network == "public" { 408 networkscope = instance.NetworkPublic 409 } 410 for _, address := range ips { 411 // Assume ipv4 unless specified otherwise 412 addrtype := instance.Ipv4Address 413 if address.Version == 6 { 414 addrtype = instance.Ipv6Address 415 } 416 machineAddr := instance.NewAddress(address.Address, networkscope) 417 machineAddr.NetworkName = network 418 if machineAddr.Type != addrtype { 419 logger.Warningf("derived address type %v, nova reports %v", machineAddr.Type, addrtype) 420 } 421 machineAddresses = append(machineAddresses, machineAddr) 422 } 423 } 424 return machineAddresses 425 } 426 427 // TODO: following 30 lines nearly verbatim from environs/ec2 428 429 func (inst *openstackInstance) OpenPorts(machineId string, ports []instance.Port) error { 430 if inst.e.Config().FirewallMode() != config.FwInstance { 431 return fmt.Errorf("invalid firewall mode %q for opening ports on instance", 432 inst.e.Config().FirewallMode()) 433 } 434 name := inst.e.machineGroupName(machineId) 435 if err := inst.e.openPortsInGroup(name, ports); err != nil { 436 return err 437 } 438 logger.Infof("opened ports in security group %s: %v", name, ports) 439 return nil 440 } 441 442 func (inst *openstackInstance) ClosePorts(machineId string, ports []instance.Port) error { 443 if inst.e.Config().FirewallMode() != config.FwInstance { 444 return fmt.Errorf("invalid firewall mode %q for closing ports on instance", 445 inst.e.Config().FirewallMode()) 446 } 447 name := inst.e.machineGroupName(machineId) 448 if err := inst.e.closePortsInGroup(name, ports); err != nil { 449 return err 450 } 451 logger.Infof("closed ports in security group %s: %v", name, ports) 452 return nil 453 } 454 455 func (inst *openstackInstance) Ports(machineId string) ([]instance.Port, error) { 456 if inst.e.Config().FirewallMode() != config.FwInstance { 457 return nil, fmt.Errorf("invalid firewall mode %q for retrieving ports from instance", 458 inst.e.Config().FirewallMode()) 459 } 460 name := inst.e.machineGroupName(machineId) 461 return inst.e.portsInGroup(name) 462 } 463 464 func (e *environ) ecfg() *environConfig { 465 e.ecfgMutex.Lock() 466 ecfg := e.ecfgUnlocked 467 e.ecfgMutex.Unlock() 468 return ecfg 469 } 470 471 func (e *environ) nova() *nova.Client { 472 e.ecfgMutex.Lock() 473 nova := e.novaUnlocked 474 e.ecfgMutex.Unlock() 475 return nova 476 } 477 478 func (e *environ) Name() string { 479 return e.name 480 } 481 482 // SupportedArchitectures is specified on the EnvironCapability interface. 483 func (e *environ) SupportedArchitectures() ([]string, error) { 484 e.archMutex.Lock() 485 defer e.archMutex.Unlock() 486 if e.supportedArchitectures != nil { 487 return e.supportedArchitectures, nil 488 } 489 // Create a filter to get all images from our region and for the correct stream. 490 cloudSpec, err := e.Region() 491 if err != nil { 492 return nil, err 493 } 494 imageConstraint := imagemetadata.NewImageConstraint(simplestreams.LookupParams{ 495 CloudSpec: cloudSpec, 496 Stream: e.Config().ImageStream(), 497 }) 498 e.supportedArchitectures, err = common.SupportedArchitectures(e, imageConstraint) 499 return e.supportedArchitectures, err 500 } 501 502 // SupportNetworks is specified on the EnvironCapability interface. 503 func (e *environ) SupportNetworks() bool { 504 // TODO(dimitern) Once we have support for networking, inquire 505 // about capabilities and return true if supported. 506 return false 507 } 508 509 var unsupportedConstraints = []string{ 510 constraints.Tags, 511 constraints.CpuPower, 512 } 513 514 // ConstraintsValidator is defined on the Environs interface. 515 func (e *environ) ConstraintsValidator() (constraints.Validator, error) { 516 validator := constraints.NewValidator() 517 validator.RegisterConflicts( 518 []string{constraints.InstanceType}, 519 []string{constraints.Mem, constraints.Arch, constraints.RootDisk, constraints.CpuCores}) 520 validator.RegisterUnsupported(unsupportedConstraints) 521 supportedArches, err := e.SupportedArchitectures() 522 if err != nil { 523 return nil, err 524 } 525 validator.RegisterVocabulary(constraints.Arch, supportedArches) 526 novaClient := e.nova() 527 flavors, err := novaClient.ListFlavorsDetail() 528 if err != nil { 529 return nil, err 530 } 531 instTypeNames := make([]string, len(flavors)) 532 for i, flavor := range flavors { 533 instTypeNames[i] = flavor.Name 534 } 535 validator.RegisterVocabulary(constraints.InstanceType, instTypeNames) 536 return validator, nil 537 } 538 539 // PrecheckInstance is defined on the state.Prechecker interface. 540 func (e *environ) PrecheckInstance(series string, cons constraints.Value, placement string) error { 541 if placement != "" { 542 return fmt.Errorf("unknown placement directive: %s", placement) 543 } 544 if !cons.HasInstanceType() { 545 return nil 546 } 547 // Constraint has an instance-type constraint so let's see if it is valid. 548 novaClient := e.nova() 549 flavors, err := novaClient.ListFlavorsDetail() 550 if err != nil { 551 return err 552 } 553 for _, flavor := range flavors { 554 if flavor.Name == *cons.InstanceType { 555 return nil 556 } 557 } 558 return fmt.Errorf("invalid Openstack flavour %q specified", *cons.InstanceType) 559 } 560 561 func (e *environ) Storage() storage.Storage { 562 e.ecfgMutex.Lock() 563 stor := e.storageUnlocked 564 e.ecfgMutex.Unlock() 565 return stor 566 } 567 568 func (e *environ) Bootstrap(ctx environs.BootstrapContext, args environs.BootstrapParams) error { 569 // The client's authentication may have been reset when finding tools if the agent-version 570 // attribute was updated so we need to re-authenticate. This will be a no-op if already authenticated. 571 // An authenticated client is needed for the URL() call below. 572 err := e.client.Authenticate() 573 if err != nil { 574 return err 575 } 576 return common.Bootstrap(ctx, e, args) 577 } 578 579 func (e *environ) StateInfo() (*state.Info, *api.Info, error) { 580 return common.StateInfo(e) 581 } 582 583 func (e *environ) Config() *config.Config { 584 return e.ecfg().Config 585 } 586 587 func (e *environ) authClient(ecfg *environConfig, authModeCfg AuthMode) client.AuthenticatingClient { 588 cred := &identity.Credentials{ 589 User: ecfg.username(), 590 Secrets: ecfg.password(), 591 Region: ecfg.region(), 592 TenantName: ecfg.tenantName(), 593 URL: ecfg.authURL(), 594 } 595 // authModeCfg has already been validated so we know it's one of the values below. 596 var authMode identity.AuthMode 597 switch authModeCfg { 598 case AuthLegacy: 599 authMode = identity.AuthLegacy 600 case AuthUserPass: 601 authMode = identity.AuthUserPass 602 case AuthKeyPair: 603 authMode = identity.AuthKeyPair 604 cred.User = ecfg.accessKey() 605 cred.Secrets = ecfg.secretKey() 606 } 607 newClient := client.NewClient 608 if !ecfg.SSLHostnameVerification() { 609 newClient = client.NewNonValidatingClient 610 } 611 return newClient(cred, authMode, nil) 612 } 613 614 func (e *environ) SetConfig(cfg *config.Config) error { 615 ecfg, err := providerInstance.newConfig(cfg) 616 if err != nil { 617 return err 618 } 619 // At this point, the authentication method config value has been validated so we extract it's value here 620 // to avoid having to validate again each time when creating the OpenStack client. 621 var authModeCfg AuthMode 622 e.ecfgMutex.Lock() 623 defer e.ecfgMutex.Unlock() 624 authModeCfg = AuthMode(ecfg.authMode()) 625 e.ecfgUnlocked = ecfg 626 627 e.client = e.authClient(ecfg, authModeCfg) 628 e.novaUnlocked = nova.New(e.client) 629 630 // create new control storage instance, existing instances continue 631 // to reference their existing configuration. 632 // public storage instance creation is deferred until needed since authenticated 633 // access to the identity service is required so that any juju-tools endpoint can be used. 634 e.storageUnlocked = &openstackstorage{ 635 containerName: ecfg.controlBucket(), 636 // this is possibly just a hack - if the ACL is swift.Private, 637 // the machine won't be able to get the tools (401 error) 638 containerACL: swift.PublicRead, 639 swift: swift.New(e.client)} 640 return nil 641 } 642 643 // GetImageSources returns a list of sources which are used to search for simplestreams image metadata. 644 func (e *environ) GetImageSources() ([]simplestreams.DataSource, error) { 645 e.imageBaseMutex.Lock() 646 defer e.imageBaseMutex.Unlock() 647 648 if e.imageSources != nil { 649 return e.imageSources, nil 650 } 651 if !e.client.IsAuthenticated() { 652 err := e.client.Authenticate() 653 if err != nil { 654 return nil, err 655 } 656 } 657 // Add the simplestreams source off the control bucket. 658 e.imageSources = append(e.imageSources, storage.NewStorageSimpleStreamsDataSource( 659 "cloud storage", e.Storage(), storage.BaseImagesPath)) 660 // Add the simplestreams base URL from keystone if it is defined. 661 productStreamsURL, err := e.client.MakeServiceURL("product-streams", nil) 662 if err == nil { 663 verify := utils.VerifySSLHostnames 664 if !e.Config().SSLHostnameVerification() { 665 verify = utils.NoVerifySSLHostnames 666 } 667 source := simplestreams.NewURLDataSource("keystone catalog", productStreamsURL, verify) 668 e.imageSources = append(e.imageSources, source) 669 } 670 return e.imageSources, nil 671 } 672 673 // GetToolsSources returns a list of sources which are used to search for simplestreams tools metadata. 674 func (e *environ) GetToolsSources() ([]simplestreams.DataSource, error) { 675 e.toolsBaseMutex.Lock() 676 defer e.toolsBaseMutex.Unlock() 677 678 if e.toolsSources != nil { 679 return e.toolsSources, nil 680 } 681 if !e.client.IsAuthenticated() { 682 err := e.client.Authenticate() 683 if err != nil { 684 return nil, err 685 } 686 } 687 verify := utils.VerifySSLHostnames 688 if !e.Config().SSLHostnameVerification() { 689 verify = utils.NoVerifySSLHostnames 690 } 691 // Add the simplestreams source off the control bucket. 692 e.toolsSources = append(e.toolsSources, storage.NewStorageSimpleStreamsDataSource( 693 "cloud storage", e.Storage(), storage.BaseToolsPath)) 694 // Add the simplestreams base URL from keystone if it is defined. 695 toolsURL, err := e.client.MakeServiceURL("juju-tools", nil) 696 if err == nil { 697 source := simplestreams.NewURLDataSource("keystone catalog", toolsURL, verify) 698 e.toolsSources = append(e.toolsSources, source) 699 } 700 return e.toolsSources, nil 701 } 702 703 // TODO(gz): Move this somewhere more reusable 704 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})$" 705 706 var uuidRegexp = regexp.MustCompile(uuidPattern) 707 708 // resolveNetwork takes either a network id or label and returns a network id 709 func (e *environ) resolveNetwork(networkName string) (string, error) { 710 if uuidRegexp.MatchString(networkName) { 711 // Network id supplied, assume valid as boot will fail if not 712 return networkName, nil 713 } 714 // Network label supplied, resolve to a network id 715 networks, err := e.nova().ListNetworks() 716 if err != nil { 717 return "", err 718 } 719 var networkIds = []string{} 720 for _, network := range networks { 721 if network.Label == networkName { 722 networkIds = append(networkIds, network.Id) 723 } 724 } 725 switch len(networkIds) { 726 case 1: 727 return networkIds[0], nil 728 case 0: 729 return "", fmt.Errorf("No networks exist with label %q", networkName) 730 } 731 return "", fmt.Errorf("Multiple networks with label %q: %v", networkName, networkIds) 732 } 733 734 // allocatePublicIP tries to find an available floating IP address, or 735 // allocates a new one, returning it, or an error 736 func (e *environ) allocatePublicIP() (*nova.FloatingIP, error) { 737 fips, err := e.nova().ListFloatingIPs() 738 if err != nil { 739 return nil, err 740 } 741 var newfip *nova.FloatingIP 742 for _, fip := range fips { 743 newfip = &fip 744 if fip.InstanceId != nil && *fip.InstanceId != "" { 745 // unavailable, skip 746 newfip = nil 747 continue 748 } else { 749 // unassigned, we can use it 750 return newfip, nil 751 } 752 } 753 if newfip == nil { 754 // allocate a new IP and use it 755 newfip, err = e.nova().AllocateFloatingIP() 756 if err != nil { 757 return nil, err 758 } 759 } 760 return newfip, nil 761 } 762 763 // assignPublicIP tries to assign the given floating IP address to the 764 // specified server, or returns an error. 765 func (e *environ) assignPublicIP(fip *nova.FloatingIP, serverId string) (err error) { 766 if fip == nil { 767 return fmt.Errorf("cannot assign a nil public IP to %q", serverId) 768 } 769 if fip.InstanceId != nil && *fip.InstanceId == serverId { 770 // IP already assigned, nothing to do 771 return nil 772 } 773 // At startup nw_info is not yet cached so this may fail 774 // temporarily while the server is being built 775 for a := common.LongAttempt.Start(); a.Next(); { 776 err = e.nova().AddServerFloatingIP(serverId, fip.IP) 777 if err == nil { 778 return nil 779 } 780 } 781 return err 782 } 783 784 // StartInstance is specified in the InstanceBroker interface. 785 func (e *environ) StartInstance(args environs.StartInstanceParams) (instance.Instance, *instance.HardwareCharacteristics, []network.Info, error) { 786 787 if args.MachineConfig.HasNetworks() { 788 return nil, nil, nil, fmt.Errorf("starting instances with networks is not supported yet.") 789 } 790 791 series := args.Tools.OneSeries() 792 arches := args.Tools.Arches() 793 spec, err := findInstanceSpec(e, &instances.InstanceConstraint{ 794 Region: e.ecfg().region(), 795 Series: series, 796 Arches: arches, 797 Constraints: args.Constraints, 798 }) 799 if err != nil { 800 return nil, nil, nil, err 801 } 802 tools, err := args.Tools.Match(tools.Filter{Arch: spec.Image.Arch}) 803 if err != nil { 804 return nil, nil, nil, fmt.Errorf("chosen architecture %v not present in %v", spec.Image.Arch, arches) 805 } 806 807 args.MachineConfig.Tools = tools[0] 808 809 if err := environs.FinishMachineConfig(args.MachineConfig, e.Config(), args.Constraints); err != nil { 810 return nil, nil, nil, err 811 } 812 userData, err := environs.ComposeUserData(args.MachineConfig, nil) 813 if err != nil { 814 return nil, nil, nil, fmt.Errorf("cannot make user data: %v", err) 815 } 816 logger.Debugf("openstack user data; %d bytes", len(userData)) 817 var networks = []nova.ServerNetworks{} 818 usingNetwork := e.ecfg().network() 819 if usingNetwork != "" { 820 networkId, err := e.resolveNetwork(usingNetwork) 821 if err != nil { 822 return nil, nil, nil, err 823 } 824 logger.Debugf("using network id %q", networkId) 825 networks = append(networks, nova.ServerNetworks{NetworkId: networkId}) 826 } 827 withPublicIP := e.ecfg().useFloatingIP() 828 var publicIP *nova.FloatingIP 829 if withPublicIP { 830 if fip, err := e.allocatePublicIP(); err != nil { 831 return nil, nil, nil, fmt.Errorf("cannot allocate a public IP as needed: %v", err) 832 } else { 833 publicIP = fip 834 logger.Infof("allocated public IP %s", publicIP.IP) 835 } 836 } 837 cfg := e.Config() 838 groups, err := e.setUpGroups(args.MachineConfig.MachineId, cfg.StatePort(), cfg.APIPort()) 839 if err != nil { 840 return nil, nil, nil, fmt.Errorf("cannot set up groups: %v", err) 841 } 842 var groupNames = make([]nova.SecurityGroupName, len(groups)) 843 for i, g := range groups { 844 groupNames[i] = nova.SecurityGroupName{g.Name} 845 } 846 var opts = nova.RunServerOpts{ 847 Name: e.machineFullName(args.MachineConfig.MachineId), 848 FlavorId: spec.InstanceType.Id, 849 ImageId: spec.Image.Id, 850 UserData: userData, 851 SecurityGroupNames: groupNames, 852 Networks: networks, 853 } 854 var server *nova.Entity 855 for a := shortAttempt.Start(); a.Next(); { 856 server, err = e.nova().RunServer(opts) 857 if err == nil || !gooseerrors.IsNotFound(err) { 858 break 859 } 860 } 861 if err != nil { 862 return nil, nil, nil, fmt.Errorf("cannot run instance: %v", err) 863 } 864 detail, err := e.nova().GetServer(server.Id) 865 if err != nil { 866 return nil, nil, nil, fmt.Errorf("cannot get started instance: %v", err) 867 } 868 inst := &openstackInstance{ 869 e: e, 870 serverDetail: detail, 871 arch: &spec.Image.Arch, 872 instType: &spec.InstanceType, 873 } 874 logger.Infof("started instance %q", inst.Id()) 875 if withPublicIP { 876 if err := e.assignPublicIP(publicIP, string(inst.Id())); err != nil { 877 if err := e.terminateInstances([]instance.Id{inst.Id()}); err != nil { 878 // ignore the failure at this stage, just log it 879 logger.Debugf("failed to terminate instance %q: %v", inst.Id(), err) 880 } 881 return nil, nil, nil, fmt.Errorf("cannot assign public address %s to instance %q: %v", publicIP.IP, inst.Id(), err) 882 } 883 logger.Infof("assigned public IP %s to %q", publicIP.IP, inst.Id()) 884 } 885 return inst, inst.hardwareCharacteristics(), nil, nil 886 } 887 888 func (e *environ) StopInstances(ids ...instance.Id) error { 889 // If in instance firewall mode, gather the security group names. 890 var securityGroupNames []string 891 if e.Config().FirewallMode() == config.FwInstance { 892 instances, err := e.Instances(ids) 893 if err == environs.ErrNoInstances { 894 return nil 895 } 896 securityGroupNames = make([]string, 0, len(ids)) 897 for _, inst := range instances { 898 if inst == nil { 899 continue 900 } 901 openstackName := inst.(*openstackInstance).getServerDetail().Name 902 lastDashPos := strings.LastIndex(openstackName, "-") 903 if lastDashPos == -1 { 904 return fmt.Errorf("cannot identify machine ID in openstack server name %q", openstackName) 905 } 906 securityGroupName := e.machineGroupName(openstackName[lastDashPos+1:]) 907 securityGroupNames = append(securityGroupNames, securityGroupName) 908 } 909 } 910 logger.Debugf("terminating instances %v", ids) 911 if err := e.terminateInstances(ids); err != nil { 912 return err 913 } 914 if securityGroupNames != nil { 915 return e.deleteSecurityGroups(securityGroupNames) 916 } 917 return nil 918 } 919 920 // collectInstances tries to get information on each instance id in ids. 921 // It fills the slots in the given map for known servers with status 922 // either ACTIVE or BUILD. Returns a list of missing ids. 923 func (e *environ) collectInstances(ids []instance.Id, out map[instance.Id]instance.Instance) []instance.Id { 924 var err error 925 serversById := make(map[string]nova.ServerDetail) 926 if len(ids) == 1 { 927 // most common case - single instance 928 var server *nova.ServerDetail 929 server, err = e.nova().GetServer(string(ids[0])) 930 if server != nil { 931 serversById[server.Id] = *server 932 } 933 } else { 934 var servers []nova.ServerDetail 935 servers, err = e.nova().ListServersDetail(e.machinesFilter()) 936 for _, server := range servers { 937 serversById[server.Id] = server 938 } 939 } 940 if err != nil { 941 return ids 942 } 943 var missing []instance.Id 944 for _, id := range ids { 945 if server, found := serversById[string(id)]; found { 946 // HPCloud uses "BUILD(spawning)" as an intermediate BUILD states once networking is available. 947 switch server.Status { 948 case nova.StatusActive, nova.StatusBuild, nova.StatusBuildSpawning: 949 // TODO(wallyworld): lookup the flavor details to fill in the instance type data 950 out[id] = &openstackInstance{e: e, serverDetail: &server} 951 continue 952 } 953 } 954 missing = append(missing, id) 955 } 956 return missing 957 } 958 959 func (e *environ) Instances(ids []instance.Id) ([]instance.Instance, error) { 960 if len(ids) == 0 { 961 return nil, nil 962 } 963 missing := ids 964 found := make(map[instance.Id]instance.Instance) 965 // Make a series of requests to cope with eventual consistency. 966 // Each request will attempt to add more instances to the requested 967 // set. 968 for a := shortAttempt.Start(); a.Next(); { 969 if missing = e.collectInstances(missing, found); len(missing) == 0 { 970 break 971 } 972 } 973 if len(found) == 0 { 974 return nil, environs.ErrNoInstances 975 } 976 insts := make([]instance.Instance, len(ids)) 977 var err error 978 for i, id := range ids { 979 if inst := found[id]; inst != nil { 980 insts[i] = inst 981 } else { 982 err = environs.ErrPartialInstances 983 } 984 } 985 return insts, err 986 } 987 988 // AllocateAddress requests a new address to be allocated for the 989 // given instance on the given network. This is not implemented on the 990 // OpenStack provider yet. 991 func (*environ) AllocateAddress(_ instance.Id, _ network.Id) (instance.Address, error) { 992 return instance.Address{}, jujuerrors.NotImplementedf("AllocateAddress") 993 } 994 995 func (e *environ) AllInstances() (insts []instance.Instance, err error) { 996 servers, err := e.nova().ListServersDetail(e.machinesFilter()) 997 if err != nil { 998 return nil, err 999 } 1000 for _, server := range servers { 1001 if server.Status == nova.StatusActive || server.Status == nova.StatusBuild { 1002 var s = server 1003 // TODO(wallyworld): lookup the flavor details to fill in the instance type data 1004 insts = append(insts, &openstackInstance{ 1005 e: e, 1006 serverDetail: &s, 1007 }) 1008 } 1009 } 1010 return insts, err 1011 } 1012 1013 func (e *environ) Destroy() error { 1014 err := common.Destroy(e) 1015 if err != nil { 1016 return err 1017 } 1018 novaClient := e.nova() 1019 securityGroups, err := novaClient.ListSecurityGroups() 1020 if err != nil { 1021 return err 1022 } 1023 re, err := regexp.Compile(fmt.Sprintf("^%s(-\\d+)?$", e.jujuGroupName())) 1024 if err != nil { 1025 return err 1026 } 1027 globalGroupName := e.globalGroupName() 1028 for _, group := range securityGroups { 1029 if re.MatchString(group.Name) || group.Name == globalGroupName { 1030 err = novaClient.DeleteSecurityGroup(group.Id) 1031 if err != nil { 1032 logger.Warningf("cannot delete security group %q. Used by another environment?", group.Name) 1033 } 1034 } 1035 } 1036 return nil 1037 } 1038 1039 func (e *environ) globalGroupName() string { 1040 return fmt.Sprintf("%s-global", e.jujuGroupName()) 1041 } 1042 1043 func (e *environ) machineGroupName(machineId string) string { 1044 return fmt.Sprintf("%s-%s", e.jujuGroupName(), machineId) 1045 } 1046 1047 func (e *environ) jujuGroupName() string { 1048 return fmt.Sprintf("juju-%s", e.name) 1049 } 1050 1051 func (e *environ) machineFullName(machineId string) string { 1052 return fmt.Sprintf("juju-%s-%s", e.Name(), names.MachineTag(machineId)) 1053 } 1054 1055 // machinesFilter returns a nova.Filter matching all machines in the environment. 1056 func (e *environ) machinesFilter() *nova.Filter { 1057 filter := nova.NewFilter() 1058 filter.Set(nova.FilterServer, fmt.Sprintf("juju-%s-machine-\\d*", e.Name())) 1059 return filter 1060 } 1061 1062 func (e *environ) openPortsInGroup(name string, ports []instance.Port) error { 1063 novaclient := e.nova() 1064 group, err := novaclient.SecurityGroupByName(name) 1065 if err != nil { 1066 return err 1067 } 1068 for _, port := range ports { 1069 _, err := novaclient.CreateSecurityGroupRule(nova.RuleInfo{ 1070 ParentGroupId: group.Id, 1071 FromPort: port.Number, 1072 ToPort: port.Number, 1073 IPProtocol: port.Protocol, 1074 Cidr: "0.0.0.0/0", 1075 }) 1076 if err != nil { 1077 // TODO: if err is not rule already exists, raise? 1078 logger.Debugf("error creating security group rule: %v", err.Error()) 1079 } 1080 } 1081 return nil 1082 } 1083 1084 func (e *environ) closePortsInGroup(name string, ports []instance.Port) error { 1085 if len(ports) == 0 { 1086 return nil 1087 } 1088 novaclient := e.nova() 1089 group, err := novaclient.SecurityGroupByName(name) 1090 if err != nil { 1091 return err 1092 } 1093 // TODO: Hey look ma, it's quadratic 1094 for _, port := range ports { 1095 for _, p := range (*group).Rules { 1096 if p.IPProtocol == nil || *p.IPProtocol != port.Protocol || 1097 p.FromPort == nil || *p.FromPort != port.Number || 1098 p.ToPort == nil || *p.ToPort != port.Number { 1099 continue 1100 } 1101 err := novaclient.DeleteSecurityGroupRule(p.Id) 1102 if err != nil { 1103 return err 1104 } 1105 break 1106 } 1107 } 1108 return nil 1109 } 1110 1111 func (e *environ) portsInGroup(name string) (ports []instance.Port, err error) { 1112 group, err := e.nova().SecurityGroupByName(name) 1113 if err != nil { 1114 return nil, err 1115 } 1116 for _, p := range (*group).Rules { 1117 for i := *p.FromPort; i <= *p.ToPort; i++ { 1118 ports = append(ports, instance.Port{ 1119 Protocol: *p.IPProtocol, 1120 Number: i, 1121 }) 1122 } 1123 } 1124 instance.SortPorts(ports) 1125 return ports, nil 1126 } 1127 1128 // TODO: following 30 lines nearly verbatim from environs/ec2 1129 1130 func (e *environ) OpenPorts(ports []instance.Port) error { 1131 if e.Config().FirewallMode() != config.FwGlobal { 1132 return fmt.Errorf("invalid firewall mode %q for opening ports on environment", 1133 e.Config().FirewallMode()) 1134 } 1135 if err := e.openPortsInGroup(e.globalGroupName(), ports); err != nil { 1136 return err 1137 } 1138 logger.Infof("opened ports in global group: %v", ports) 1139 return nil 1140 } 1141 1142 func (e *environ) ClosePorts(ports []instance.Port) error { 1143 if e.Config().FirewallMode() != config.FwGlobal { 1144 return fmt.Errorf("invalid firewall mode %q for closing ports on environment", 1145 e.Config().FirewallMode()) 1146 } 1147 if err := e.closePortsInGroup(e.globalGroupName(), ports); err != nil { 1148 return err 1149 } 1150 logger.Infof("closed ports in global group: %v", ports) 1151 return nil 1152 } 1153 1154 func (e *environ) Ports() ([]instance.Port, error) { 1155 if e.Config().FirewallMode() != config.FwGlobal { 1156 return nil, fmt.Errorf("invalid firewall mode %q for retrieving ports from environment", 1157 e.Config().FirewallMode()) 1158 } 1159 return e.portsInGroup(e.globalGroupName()) 1160 } 1161 1162 func (e *environ) Provider() environs.EnvironProvider { 1163 return &providerInstance 1164 } 1165 1166 func (e *environ) setUpGlobalGroup(groupName string, statePort, apiPort int) (nova.SecurityGroup, error) { 1167 return e.ensureGroup(groupName, 1168 []nova.RuleInfo{ 1169 { 1170 IPProtocol: "tcp", 1171 FromPort: 22, 1172 ToPort: 22, 1173 Cidr: "0.0.0.0/0", 1174 }, 1175 { 1176 IPProtocol: "tcp", 1177 FromPort: statePort, 1178 ToPort: statePort, 1179 Cidr: "0.0.0.0/0", 1180 }, 1181 { 1182 IPProtocol: "tcp", 1183 FromPort: apiPort, 1184 ToPort: apiPort, 1185 Cidr: "0.0.0.0/0", 1186 }, 1187 { 1188 IPProtocol: "tcp", 1189 FromPort: 1, 1190 ToPort: 65535, 1191 }, 1192 { 1193 IPProtocol: "udp", 1194 FromPort: 1, 1195 ToPort: 65535, 1196 }, 1197 { 1198 IPProtocol: "icmp", 1199 FromPort: -1, 1200 ToPort: -1, 1201 }, 1202 }) 1203 } 1204 1205 // setUpGroups creates the security groups for the new machine, and 1206 // returns them. 1207 // 1208 // Instances are tagged with a group so they can be distinguished from 1209 // other instances that might be running on the same OpenStack account. 1210 // In addition, a specific machine security group is created for each 1211 // machine, so that its firewall rules can be configured per machine. 1212 // 1213 // Note: ideally we'd have a better way to determine group membership so that 2 1214 // people that happen to share an openstack account and name their environment 1215 // "openstack" don't end up destroying each other's machines. 1216 func (e *environ) setUpGroups(machineId string, statePort, apiPort int) ([]nova.SecurityGroup, error) { 1217 jujuGroup, err := e.setUpGlobalGroup(e.jujuGroupName(), statePort, apiPort) 1218 if err != nil { 1219 return nil, err 1220 } 1221 var machineGroup nova.SecurityGroup 1222 switch e.Config().FirewallMode() { 1223 case config.FwInstance: 1224 machineGroup, err = e.ensureGroup(e.machineGroupName(machineId), nil) 1225 case config.FwGlobal: 1226 machineGroup, err = e.ensureGroup(e.globalGroupName(), nil) 1227 } 1228 if err != nil { 1229 return nil, err 1230 } 1231 groups := []nova.SecurityGroup{jujuGroup, machineGroup} 1232 if e.ecfg().useDefaultSecurityGroup() { 1233 defaultGroup, err := e.nova().SecurityGroupByName("default") 1234 if err != nil { 1235 return nil, fmt.Errorf("loading default security group: %v", err) 1236 } 1237 groups = append(groups, *defaultGroup) 1238 } 1239 return groups, nil 1240 } 1241 1242 // zeroGroup holds the zero security group. 1243 var zeroGroup nova.SecurityGroup 1244 1245 // ensureGroup returns the security group with name and perms. 1246 // If a group with name does not exist, one will be created. 1247 // If it exists, its permissions are set to perms. 1248 func (e *environ) ensureGroup(name string, rules []nova.RuleInfo) (nova.SecurityGroup, error) { 1249 novaClient := e.nova() 1250 // First attempt to look up an existing group by name. 1251 group, err := novaClient.SecurityGroupByName(name) 1252 if err == nil { 1253 // Group exists, so assume it is correctly set up and return it. 1254 // TODO(jam): 2013-09-18 http://pad.lv/121795 1255 // We really should verify the group is set up correctly, 1256 // because deleting and re-creating environments can get us bad 1257 // groups (especially if they were set up under Python) 1258 return *group, nil 1259 } 1260 // Doesn't exist, so try and create it. 1261 group, err = novaClient.CreateSecurityGroup(name, "juju group") 1262 if err != nil { 1263 if !gooseerrors.IsDuplicateValue(err) { 1264 return zeroGroup, err 1265 } else { 1266 // We just tried to create a duplicate group, so load the existing group. 1267 group, err = novaClient.SecurityGroupByName(name) 1268 if err != nil { 1269 return zeroGroup, err 1270 } 1271 return *group, nil 1272 } 1273 } 1274 // The new group is created so now add the rules. 1275 group.Rules = make([]nova.SecurityGroupRule, len(rules)) 1276 for i, rule := range rules { 1277 rule.ParentGroupId = group.Id 1278 if rule.Cidr == "" { 1279 // http://pad.lv/1226996 Rules that don't have a CIDR 1280 // are meant to apply only to this group. If you don't 1281 // supply CIDR or GroupId then openstack assumes you 1282 // mean CIDR=0.0.0.0/0 1283 rule.GroupId = &group.Id 1284 } 1285 groupRule, err := novaClient.CreateSecurityGroupRule(rule) 1286 if err != nil && !gooseerrors.IsDuplicateValue(err) { 1287 return zeroGroup, err 1288 } 1289 group.Rules[i] = *groupRule 1290 } 1291 return *group, nil 1292 } 1293 1294 // deleteSecurityGroups deletes the given security groups. If a security 1295 // group is also used by another environment (see bug #1300755), an attempt 1296 // to delete this group fails. A warning is logged in this case. 1297 func (e *environ) deleteSecurityGroups(securityGroupNames []string) error { 1298 novaclient := e.nova() 1299 allSecurityGroups, err := novaclient.ListSecurityGroups() 1300 if err != nil { 1301 return err 1302 } 1303 for _, securityGroup := range allSecurityGroups { 1304 for _, name := range securityGroupNames { 1305 if securityGroup.Name == name { 1306 err := novaclient.DeleteSecurityGroup(securityGroup.Id) 1307 if err != nil { 1308 logger.Warningf("cannot delete security group %q. Used by another environment?", name) 1309 } 1310 break 1311 } 1312 } 1313 } 1314 return nil 1315 } 1316 1317 func (e *environ) terminateInstances(ids []instance.Id) error { 1318 if len(ids) == 0 { 1319 return nil 1320 } 1321 var firstErr error 1322 novaClient := e.nova() 1323 for _, id := range ids { 1324 err := novaClient.DeleteServer(string(id)) 1325 if gooseerrors.IsNotFound(err) { 1326 err = nil 1327 } 1328 if err != nil && firstErr == nil { 1329 logger.Debugf("error terminating instance %q: %v", id, err) 1330 firstErr = err 1331 } 1332 } 1333 return firstErr 1334 } 1335 1336 // MetadataLookupParams returns parameters which are used to query simplestreams metadata. 1337 func (e *environ) MetadataLookupParams(region string) (*simplestreams.MetadataLookupParams, error) { 1338 if region == "" { 1339 region = e.ecfg().region() 1340 } 1341 cloudSpec, err := e.cloudSpec(region) 1342 if err != nil { 1343 return nil, err 1344 } 1345 return &simplestreams.MetadataLookupParams{ 1346 Series: config.PreferredSeries(e.ecfg()), 1347 Region: cloudSpec.Region, 1348 Endpoint: cloudSpec.Endpoint, 1349 Architectures: arch.AllSupportedArches, 1350 }, nil 1351 } 1352 1353 // Region is specified in the HasRegion interface. 1354 func (e *environ) Region() (simplestreams.CloudSpec, error) { 1355 return e.cloudSpec(e.ecfg().region()) 1356 } 1357 1358 func (e *environ) cloudSpec(region string) (simplestreams.CloudSpec, error) { 1359 return simplestreams.CloudSpec{ 1360 Region: region, 1361 Endpoint: e.ecfg().authURL(), 1362 }, nil 1363 }