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