github.com/Pankov404/juju@v0.0.0-20150703034450-be266991dceb/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 "gopkg.in/goose.v1/client" 22 gooseerrors "gopkg.in/goose.v1/errors" 23 "gopkg.in/goose.v1/identity" 24 "gopkg.in/goose.v1/nova" 25 "gopkg.in/goose.v1/swift" 26 27 "github.com/juju/juju/cloudconfig/instancecfg" 28 "github.com/juju/juju/cloudconfig/providerinit" 29 "github.com/juju/juju/constraints" 30 "github.com/juju/juju/environs" 31 "github.com/juju/juju/environs/config" 32 "github.com/juju/juju/environs/imagemetadata" 33 "github.com/juju/juju/environs/instances" 34 "github.com/juju/juju/environs/simplestreams" 35 "github.com/juju/juju/environs/storage" 36 "github.com/juju/juju/instance" 37 "github.com/juju/juju/juju/arch" 38 "github.com/juju/juju/network" 39 "github.com/juju/juju/provider/common" 40 "github.com/juju/juju/state" 41 "github.com/juju/juju/state/multiwatcher" 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 370 type openstackInstance struct { 371 e *environ 372 instType *instances.InstanceType 373 arch *string 374 375 mu sync.Mutex 376 serverDetail *nova.ServerDetail 377 // floatingIP is non-nil iff use-floating-ip is true. 378 floatingIP *nova.FloatingIP 379 } 380 381 func (inst *openstackInstance) String() string { 382 return string(inst.Id()) 383 } 384 385 var _ instance.Instance = (*openstackInstance)(nil) 386 387 func (inst *openstackInstance) Refresh() error { 388 inst.mu.Lock() 389 defer inst.mu.Unlock() 390 server, err := inst.e.nova().GetServer(inst.serverDetail.Id) 391 if err != nil { 392 return err 393 } 394 inst.serverDetail = server 395 return nil 396 } 397 398 func (inst *openstackInstance) getServerDetail() *nova.ServerDetail { 399 inst.mu.Lock() 400 defer inst.mu.Unlock() 401 return inst.serverDetail 402 } 403 404 func (inst *openstackInstance) Id() instance.Id { 405 return instance.Id(inst.getServerDetail().Id) 406 } 407 408 func (inst *openstackInstance) Status() string { 409 return inst.getServerDetail().Status 410 } 411 412 func (inst *openstackInstance) hardwareCharacteristics() *instance.HardwareCharacteristics { 413 hc := &instance.HardwareCharacteristics{Arch: inst.arch} 414 if inst.instType != nil { 415 hc.Mem = &inst.instType.Mem 416 // openstack is special in that a 0-size root disk means that 417 // the root disk will result in an instance with a root disk 418 // the same size as the image that created it, so we just set 419 // the HardwareCharacteristics to nil to signal that we don't 420 // know what the correct size is. 421 if inst.instType.RootDisk == 0 { 422 hc.RootDisk = nil 423 } else { 424 hc.RootDisk = &inst.instType.RootDisk 425 } 426 hc.CpuCores = &inst.instType.CpuCores 427 hc.CpuPower = inst.instType.CpuPower 428 // tags not currently supported on openstack 429 } 430 hc.AvailabilityZone = &inst.serverDetail.AvailabilityZone 431 return hc 432 } 433 434 // getAddresses returns the existing server information on addresses, 435 // but fetches the details over the api again if no addresses exist. 436 func (inst *openstackInstance) getAddresses() (map[string][]nova.IPAddress, error) { 437 addrs := inst.getServerDetail().Addresses 438 if len(addrs) == 0 { 439 server, err := inst.e.nova().GetServer(string(inst.Id())) 440 if err != nil { 441 return nil, err 442 } 443 addrs = server.Addresses 444 } 445 return addrs, nil 446 } 447 448 // Addresses implements network.Addresses() returning generic address 449 // details for the instances, and calling the openstack api if needed. 450 func (inst *openstackInstance) Addresses() ([]network.Address, error) { 451 addresses, err := inst.getAddresses() 452 if err != nil { 453 return nil, err 454 } 455 var floatingIP string 456 if inst.floatingIP != nil && inst.floatingIP.IP != "" { 457 floatingIP = inst.floatingIP.IP 458 logger.Debugf("instance %v has floating IP address: %v", inst.Id(), floatingIP) 459 } 460 return convertNovaAddresses(floatingIP, addresses), nil 461 } 462 463 // convertNovaAddresses returns nova addresses in generic format 464 func convertNovaAddresses(publicIP string, addresses map[string][]nova.IPAddress) []network.Address { 465 var machineAddresses []network.Address 466 if publicIP != "" { 467 publicAddr := network.NewScopedAddress(publicIP, network.ScopePublic) 468 publicAddr.NetworkName = "public" 469 machineAddresses = append(machineAddresses, publicAddr) 470 } 471 // TODO(gz) Network ordering may be significant but is not preserved by 472 // the map, see lp:1188126 for example. That could potentially be fixed 473 // in goose, or left to be derived by other means. 474 for netName, ips := range addresses { 475 networkScope := network.ScopeUnknown 476 if netName == "public" { 477 networkScope = network.ScopePublic 478 } 479 for _, address := range ips { 480 // If this address has already been added as a floating IP, skip it. 481 if publicIP == address.Address { 482 continue 483 } 484 // Assume IPv4 unless specified otherwise 485 addrtype := network.IPv4Address 486 if address.Version == 6 { 487 addrtype = network.IPv6Address 488 } 489 machineAddr := network.NewScopedAddress(address.Address, networkScope) 490 machineAddr.NetworkName = netName 491 if machineAddr.Type != addrtype { 492 logger.Warningf("derived address type %v, nova reports %v", machineAddr.Type, addrtype) 493 } 494 machineAddresses = append(machineAddresses, machineAddr) 495 } 496 } 497 return machineAddresses 498 } 499 500 // TODO: following 30 lines nearly verbatim from environs/ec2 501 502 func (inst *openstackInstance) OpenPorts(machineId string, ports []network.PortRange) error { 503 if inst.e.Config().FirewallMode() != config.FwInstance { 504 return fmt.Errorf("invalid firewall mode %q for opening ports on instance", 505 inst.e.Config().FirewallMode()) 506 } 507 name := inst.e.machineGroupName(machineId) 508 if err := inst.e.openPortsInGroup(name, ports); err != nil { 509 return err 510 } 511 logger.Infof("opened ports in security group %s: %v", name, ports) 512 return nil 513 } 514 515 func (inst *openstackInstance) ClosePorts(machineId string, ports []network.PortRange) error { 516 if inst.e.Config().FirewallMode() != config.FwInstance { 517 return fmt.Errorf("invalid firewall mode %q for closing ports on instance", 518 inst.e.Config().FirewallMode()) 519 } 520 name := inst.e.machineGroupName(machineId) 521 if err := inst.e.closePortsInGroup(name, ports); err != nil { 522 return err 523 } 524 logger.Infof("closed ports in security group %s: %v", name, ports) 525 return nil 526 } 527 528 func (inst *openstackInstance) Ports(machineId string) ([]network.PortRange, error) { 529 if inst.e.Config().FirewallMode() != config.FwInstance { 530 return nil, fmt.Errorf("invalid firewall mode %q for retrieving ports from instance", 531 inst.e.Config().FirewallMode()) 532 } 533 name := inst.e.machineGroupName(machineId) 534 portRanges, err := inst.e.portsInGroup(name) 535 if err != nil { 536 return nil, err 537 } 538 return portRanges, nil 539 } 540 541 func (e *environ) ecfg() *environConfig { 542 e.ecfgMutex.Lock() 543 ecfg := e.ecfgUnlocked 544 e.ecfgMutex.Unlock() 545 return ecfg 546 } 547 548 func (e *environ) nova() *nova.Client { 549 e.ecfgMutex.Lock() 550 nova := e.novaUnlocked 551 e.ecfgMutex.Unlock() 552 return nova 553 } 554 555 // SupportedArchitectures is specified on the EnvironCapability interface. 556 func (e *environ) SupportedArchitectures() ([]string, error) { 557 e.archMutex.Lock() 558 defer e.archMutex.Unlock() 559 if e.supportedArchitectures != nil { 560 return e.supportedArchitectures, nil 561 } 562 // Create a filter to get all images from our region and for the correct stream. 563 cloudSpec, err := e.Region() 564 if err != nil { 565 return nil, err 566 } 567 imageConstraint := imagemetadata.NewImageConstraint(simplestreams.LookupParams{ 568 CloudSpec: cloudSpec, 569 Stream: e.Config().ImageStream(), 570 }) 571 e.supportedArchitectures, err = common.SupportedArchitectures(e, imageConstraint) 572 return e.supportedArchitectures, err 573 } 574 575 var unsupportedConstraints = []string{ 576 constraints.Tags, 577 constraints.CpuPower, 578 } 579 580 // ConstraintsValidator is defined on the Environs interface. 581 func (e *environ) ConstraintsValidator() (constraints.Validator, error) { 582 validator := constraints.NewValidator() 583 validator.RegisterConflicts( 584 []string{constraints.InstanceType}, 585 []string{constraints.Mem, constraints.Arch, constraints.RootDisk, constraints.CpuCores}) 586 validator.RegisterUnsupported(unsupportedConstraints) 587 supportedArches, err := e.SupportedArchitectures() 588 if err != nil { 589 return nil, err 590 } 591 validator.RegisterVocabulary(constraints.Arch, supportedArches) 592 novaClient := e.nova() 593 flavors, err := novaClient.ListFlavorsDetail() 594 if err != nil { 595 return nil, err 596 } 597 instTypeNames := make([]string, len(flavors)) 598 for i, flavor := range flavors { 599 instTypeNames[i] = flavor.Name 600 } 601 validator.RegisterVocabulary(constraints.InstanceType, instTypeNames) 602 return validator, nil 603 } 604 605 var novaListAvailabilityZones = (*nova.Client).ListAvailabilityZones 606 607 type openstackAvailabilityZone struct { 608 nova.AvailabilityZone 609 } 610 611 func (z *openstackAvailabilityZone) Name() string { 612 return z.AvailabilityZone.Name 613 } 614 615 func (z *openstackAvailabilityZone) Available() bool { 616 return z.AvailabilityZone.State.Available 617 } 618 619 // AvailabilityZones returns a slice of availability zones. 620 func (e *environ) AvailabilityZones() ([]common.AvailabilityZone, error) { 621 e.availabilityZonesMutex.Lock() 622 defer e.availabilityZonesMutex.Unlock() 623 if e.availabilityZones == nil { 624 zones, err := novaListAvailabilityZones(e.nova()) 625 if gooseerrors.IsNotImplemented(err) { 626 return nil, errors.NotImplementedf("availability zones") 627 } 628 if err != nil { 629 return nil, err 630 } 631 e.availabilityZones = make([]common.AvailabilityZone, len(zones)) 632 for i, z := range zones { 633 e.availabilityZones[i] = &openstackAvailabilityZone{z} 634 } 635 } 636 return e.availabilityZones, nil 637 } 638 639 // InstanceAvailabilityZoneNames returns the availability zone names for each 640 // of the specified instances. 641 func (e *environ) InstanceAvailabilityZoneNames(ids []instance.Id) ([]string, error) { 642 instances, err := e.Instances(ids) 643 if err != nil && err != environs.ErrPartialInstances { 644 return nil, err 645 } 646 zones := make([]string, len(instances)) 647 for i, inst := range instances { 648 if inst == nil { 649 continue 650 } 651 zones[i] = inst.(*openstackInstance).serverDetail.AvailabilityZone 652 } 653 return zones, err 654 } 655 656 type openstackPlacement struct { 657 availabilityZone nova.AvailabilityZone 658 } 659 660 func (e *environ) parsePlacement(placement string) (*openstackPlacement, error) { 661 pos := strings.IndexRune(placement, '=') 662 if pos == -1 { 663 return nil, fmt.Errorf("unknown placement directive: %v", placement) 664 } 665 switch key, value := placement[:pos], placement[pos+1:]; key { 666 case "zone": 667 availabilityZone := value 668 zones, err := e.AvailabilityZones() 669 if err != nil { 670 return nil, err 671 } 672 for _, z := range zones { 673 if z.Name() == availabilityZone { 674 return &openstackPlacement{ 675 z.(*openstackAvailabilityZone).AvailabilityZone, 676 }, nil 677 } 678 } 679 return nil, fmt.Errorf("invalid availability zone %q", availabilityZone) 680 } 681 return nil, fmt.Errorf("unknown placement directive: %v", placement) 682 } 683 684 // PrecheckInstance is defined on the state.Prechecker interface. 685 func (e *environ) PrecheckInstance(series string, cons constraints.Value, placement string) error { 686 if placement != "" { 687 if _, err := e.parsePlacement(placement); err != nil { 688 return err 689 } 690 } 691 if !cons.HasInstanceType() { 692 return nil 693 } 694 // Constraint has an instance-type constraint so let's see if it is valid. 695 novaClient := e.nova() 696 flavors, err := novaClient.ListFlavorsDetail() 697 if err != nil { 698 return err 699 } 700 for _, flavor := range flavors { 701 if flavor.Name == *cons.InstanceType { 702 return nil 703 } 704 } 705 return fmt.Errorf("invalid Openstack flavour %q specified", *cons.InstanceType) 706 } 707 708 func (e *environ) Storage() storage.Storage { 709 e.ecfgMutex.Lock() 710 stor := e.storageUnlocked 711 e.ecfgMutex.Unlock() 712 return stor 713 } 714 715 func (e *environ) Bootstrap(ctx environs.BootstrapContext, args environs.BootstrapParams) (arch, series string, _ environs.BootstrapFinalizer, _ error) { 716 // The client's authentication may have been reset when finding tools if the agent-version 717 // attribute was updated so we need to re-authenticate. This will be a no-op if already authenticated. 718 // An authenticated client is needed for the URL() call below. 719 if err := authenticateClient(e); err != nil { 720 return "", "", nil, err 721 } 722 return common.Bootstrap(ctx, e, args) 723 } 724 725 func (e *environ) StateServerInstances() ([]instance.Id, error) { 726 return common.ProviderStateInstances(e, e.Storage()) 727 } 728 729 func (e *environ) Config() *config.Config { 730 return e.ecfg().Config 731 } 732 733 func authClient(ecfg *environConfig) client.AuthenticatingClient { 734 cred := &identity.Credentials{ 735 User: ecfg.username(), 736 Secrets: ecfg.password(), 737 Region: ecfg.region(), 738 TenantName: ecfg.tenantName(), 739 URL: ecfg.authURL(), 740 } 741 // authModeCfg has already been validated so we know it's one of the values below. 742 var authMode identity.AuthMode 743 switch AuthMode(ecfg.authMode()) { 744 case AuthLegacy: 745 authMode = identity.AuthLegacy 746 case AuthUserPass: 747 authMode = identity.AuthUserPass 748 case AuthKeyPair: 749 authMode = identity.AuthKeyPair 750 cred.User = ecfg.accessKey() 751 cred.Secrets = ecfg.secretKey() 752 } 753 newClient := client.NewClient 754 if !ecfg.SSLHostnameVerification() { 755 newClient = client.NewNonValidatingClient 756 } 757 return newClient(cred, authMode, nil) 758 } 759 760 var authenticateClient = func(e *environ) error { 761 err := e.client.Authenticate() 762 if err != nil { 763 // Log the error in case there are any useful hints, 764 // but provide a readable and helpful error message 765 // to the user. 766 logger.Debugf("authentication failed: %v", err) 767 return errors.New(`authentication failed. 768 769 Please ensure the credentials are correct. A common mistake is 770 to specify the wrong tenant. Use the OpenStack "project" name 771 for tenant-name in your environment configuration.`) 772 } 773 return nil 774 } 775 776 func (e *environ) SetConfig(cfg *config.Config) error { 777 ecfg, err := providerInstance.newConfig(cfg) 778 if err != nil { 779 return err 780 } 781 // At this point, the authentication method config value has been validated so we extract it's value here 782 // to avoid having to validate again each time when creating the OpenStack client. 783 e.ecfgMutex.Lock() 784 defer e.ecfgMutex.Unlock() 785 e.ecfgUnlocked = ecfg 786 787 e.client = authClient(ecfg) 788 789 e.novaUnlocked = nova.New(e.client) 790 791 // create new control storage instance, existing instances continue 792 // to reference their existing configuration. 793 // public storage instance creation is deferred until needed since authenticated 794 // access to the identity service is required so that any juju-tools endpoint can be used. 795 e.storageUnlocked = &openstackstorage{ 796 containerName: ecfg.controlBucket(), 797 // this is possibly just a hack - if the ACL is swift.Private, 798 // the machine won't be able to get the tools (401 error) 799 containerACL: swift.PublicRead, 800 swift: swift.New(e.client)} 801 return nil 802 } 803 804 // getKeystoneImageSource is an imagemetadata.ImageDataSourceFunc that 805 // returns a DataSource using the "product-streams" keystone URL. 806 func getKeystoneImageSource(env environs.Environ) (simplestreams.DataSource, error) { 807 e, ok := env.(*environ) 808 if !ok { 809 return nil, errors.NotSupportedf("non-openstack environment") 810 } 811 return e.getKeystoneDataSource(&e.keystoneImageDataSourceMutex, &e.keystoneImageDataSource, "product-streams") 812 } 813 814 // getKeystoneToolsSource is a tools.ToolsDataSourceFunc that 815 // returns a DataSource using the "juju-tools" keystone URL. 816 func getKeystoneToolsSource(env environs.Environ) (simplestreams.DataSource, error) { 817 e, ok := env.(*environ) 818 if !ok { 819 return nil, errors.NotSupportedf("non-openstack environment") 820 } 821 return e.getKeystoneDataSource(&e.keystoneToolsDataSourceMutex, &e.keystoneToolsDataSource, "juju-tools") 822 } 823 824 func (e *environ) getKeystoneDataSource(mu *sync.Mutex, datasource *simplestreams.DataSource, keystoneName string) (simplestreams.DataSource, error) { 825 mu.Lock() 826 defer mu.Unlock() 827 if *datasource != nil { 828 return *datasource, nil 829 } 830 if !e.client.IsAuthenticated() { 831 if err := authenticateClient(e); err != nil { 832 return nil, err 833 } 834 } 835 836 url, err := makeServiceURL(e.client, keystoneName, nil) 837 if err != nil { 838 return nil, errors.NewNotSupported(err, fmt.Sprintf("cannot make service URL: %v", err)) 839 } 840 verify := utils.VerifySSLHostnames 841 if !e.Config().SSLHostnameVerification() { 842 verify = utils.NoVerifySSLHostnames 843 } 844 *datasource = simplestreams.NewURLDataSource("keystone catalog", url, verify) 845 return *datasource, nil 846 } 847 848 // TODO(gz): Move this somewhere more reusable 849 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})$" 850 851 var uuidRegexp = regexp.MustCompile(uuidPattern) 852 853 // resolveNetwork takes either a network id or label and returns a network id 854 func (e *environ) resolveNetwork(networkName string) (string, error) { 855 if uuidRegexp.MatchString(networkName) { 856 // Network id supplied, assume valid as boot will fail if not 857 return networkName, nil 858 } 859 // Network label supplied, resolve to a network id 860 networks, err := e.nova().ListNetworks() 861 if err != nil { 862 return "", err 863 } 864 var networkIds = []string{} 865 for _, network := range networks { 866 if network.Label == networkName { 867 networkIds = append(networkIds, network.Id) 868 } 869 } 870 switch len(networkIds) { 871 case 1: 872 return networkIds[0], nil 873 case 0: 874 return "", fmt.Errorf("No networks exist with label %q", networkName) 875 } 876 return "", fmt.Errorf("Multiple networks with label %q: %v", networkName, networkIds) 877 } 878 879 // allocatePublicIP tries to find an available floating IP address, or 880 // allocates a new one, returning it, or an error 881 func (e *environ) allocatePublicIP() (*nova.FloatingIP, error) { 882 fips, err := e.nova().ListFloatingIPs() 883 if err != nil { 884 return nil, err 885 } 886 var newfip *nova.FloatingIP 887 for _, fip := range fips { 888 newfip = &fip 889 if fip.InstanceId != nil && *fip.InstanceId != "" { 890 // unavailable, skip 891 newfip = nil 892 continue 893 } else { 894 logger.Debugf("found unassigned public ip: %v", newfip.IP) 895 // unassigned, we can use it 896 return newfip, nil 897 } 898 } 899 if newfip == nil { 900 // allocate a new IP and use it 901 newfip, err = e.nova().AllocateFloatingIP() 902 if err != nil { 903 return nil, err 904 } 905 logger.Debugf("allocated new public IP: %v", newfip.IP) 906 } 907 return newfip, nil 908 } 909 910 // assignPublicIP tries to assign the given floating IP address to the 911 // specified server, or returns an error. 912 func (e *environ) assignPublicIP(fip *nova.FloatingIP, serverId string) (err error) { 913 if fip == nil { 914 return fmt.Errorf("cannot assign a nil public IP to %q", serverId) 915 } 916 if fip.InstanceId != nil && *fip.InstanceId == serverId { 917 // IP already assigned, nothing to do 918 return nil 919 } 920 // At startup nw_info is not yet cached so this may fail 921 // temporarily while the server is being built 922 for a := common.LongAttempt.Start(); a.Next(); { 923 err = e.nova().AddServerFloatingIP(serverId, fip.IP) 924 if err == nil { 925 return nil 926 } 927 } 928 return err 929 } 930 931 // DistributeInstances implements the state.InstanceDistributor policy. 932 func (e *environ) DistributeInstances(candidates, distributionGroup []instance.Id) ([]instance.Id, error) { 933 return common.DistributeInstances(e, candidates, distributionGroup) 934 } 935 936 var availabilityZoneAllocations = common.AvailabilityZoneAllocations 937 938 // MaintainInstance is specified in the InstanceBroker interface. 939 func (*environ) MaintainInstance(args environs.StartInstanceParams) error { 940 return nil 941 } 942 943 // StartInstance is specified in the InstanceBroker interface. 944 func (e *environ) StartInstance(args environs.StartInstanceParams) (*environs.StartInstanceResult, error) { 945 var availabilityZones []string 946 if args.Placement != "" { 947 placement, err := e.parsePlacement(args.Placement) 948 if err != nil { 949 return nil, err 950 } 951 if !placement.availabilityZone.State.Available { 952 return nil, fmt.Errorf("availability zone %q is unavailable", placement.availabilityZone.Name) 953 } 954 availabilityZones = append(availabilityZones, placement.availabilityZone.Name) 955 } 956 957 // If no availability zone is specified, then automatically spread across 958 // the known zones for optimal spread across the instance distribution 959 // group. 960 if len(availabilityZones) == 0 { 961 var group []instance.Id 962 var err error 963 if args.DistributionGroup != nil { 964 group, err = args.DistributionGroup() 965 if err != nil { 966 return nil, err 967 } 968 } 969 zoneInstances, err := availabilityZoneAllocations(e, group) 970 if errors.IsNotImplemented(err) { 971 // Availability zones are an extension, so we may get a 972 // not implemented error; ignore these. 973 } else if err != nil { 974 return nil, err 975 } else { 976 for _, zone := range zoneInstances { 977 availabilityZones = append(availabilityZones, zone.ZoneName) 978 } 979 } 980 if len(availabilityZones) == 0 { 981 // No explicitly selectable zones available, so use an unspecified zone. 982 availabilityZones = []string{""} 983 } 984 } 985 986 if args.InstanceConfig.HasNetworks() { 987 return nil, fmt.Errorf("starting instances with networks is not supported yet.") 988 } 989 990 series := args.Tools.OneSeries() 991 arches := args.Tools.Arches() 992 spec, err := findInstanceSpec(e, &instances.InstanceConstraint{ 993 Region: e.ecfg().region(), 994 Series: series, 995 Arches: arches, 996 Constraints: args.Constraints, 997 }) 998 if err != nil { 999 return nil, err 1000 } 1001 tools, err := args.Tools.Match(tools.Filter{Arch: spec.Image.Arch}) 1002 if err != nil { 1003 return nil, fmt.Errorf("chosen architecture %v not present in %v", spec.Image.Arch, arches) 1004 } 1005 1006 args.InstanceConfig.Tools = tools[0] 1007 1008 if err := instancecfg.FinishInstanceConfig(args.InstanceConfig, e.Config()); err != nil { 1009 return nil, err 1010 } 1011 userData, err := providerinit.ComposeUserData(args.InstanceConfig, nil) 1012 if err != nil { 1013 return nil, fmt.Errorf("cannot make user data: %v", err) 1014 } 1015 logger.Debugf("openstack user data; %d bytes", len(userData)) 1016 1017 var networks = []nova.ServerNetworks{} 1018 usingNetwork := e.ecfg().network() 1019 if usingNetwork != "" { 1020 networkId, err := e.resolveNetwork(usingNetwork) 1021 if err != nil { 1022 return nil, err 1023 } 1024 logger.Debugf("using network id %q", networkId) 1025 networks = append(networks, nova.ServerNetworks{NetworkId: networkId}) 1026 } 1027 withPublicIP := e.ecfg().useFloatingIP() 1028 var publicIP *nova.FloatingIP 1029 if withPublicIP { 1030 logger.Debugf("allocating public IP address for openstack node") 1031 if fip, err := e.allocatePublicIP(); err != nil { 1032 return nil, fmt.Errorf("cannot allocate a public IP as needed: %v", err) 1033 } else { 1034 publicIP = fip 1035 logger.Infof("allocated public IP %s", publicIP.IP) 1036 } 1037 } 1038 1039 cfg := e.Config() 1040 groups, err := e.setUpGroups(args.InstanceConfig.MachineId, cfg.APIPort()) 1041 if err != nil { 1042 return nil, fmt.Errorf("cannot set up groups: %v", err) 1043 } 1044 var groupNames = make([]nova.SecurityGroupName, len(groups)) 1045 for i, g := range groups { 1046 groupNames[i] = nova.SecurityGroupName{g.Name} 1047 } 1048 1049 machineName := resourceName( 1050 names.NewMachineTag(args.InstanceConfig.MachineId), 1051 e.Config().Name(), 1052 ) 1053 1054 var server *nova.Entity 1055 for _, availZone := range availabilityZones { 1056 var opts = nova.RunServerOpts{ 1057 Name: machineName, 1058 FlavorId: spec.InstanceType.Id, 1059 ImageId: spec.Image.Id, 1060 UserData: userData, 1061 SecurityGroupNames: groupNames, 1062 Networks: networks, 1063 AvailabilityZone: availZone, 1064 Metadata: args.InstanceConfig.Tags, 1065 } 1066 for a := shortAttempt.Start(); a.Next(); { 1067 server, err = e.nova().RunServer(opts) 1068 if err == nil || !gooseerrors.IsNotFound(err) { 1069 break 1070 } 1071 } 1072 if isNoValidHostsError(err) { 1073 logger.Infof("no valid hosts available in zone %q, trying another availability zone", availZone) 1074 } else { 1075 break 1076 } 1077 } 1078 if err != nil { 1079 return nil, fmt.Errorf("cannot run instance: %v", err) 1080 } 1081 detail, err := e.nova().GetServer(server.Id) 1082 if err != nil { 1083 return nil, fmt.Errorf("cannot get started instance: %v", err) 1084 } 1085 inst := &openstackInstance{ 1086 e: e, 1087 serverDetail: detail, 1088 arch: &spec.Image.Arch, 1089 instType: &spec.InstanceType, 1090 } 1091 logger.Infof("started instance %q", inst.Id()) 1092 if withPublicIP { 1093 if err := e.assignPublicIP(publicIP, string(inst.Id())); err != nil { 1094 if err := e.terminateInstances([]instance.Id{inst.Id()}); err != nil { 1095 // ignore the failure at this stage, just log it 1096 logger.Debugf("failed to terminate instance %q: %v", inst.Id(), err) 1097 } 1098 return nil, fmt.Errorf("cannot assign public address %s to instance %q: %v", publicIP.IP, inst.Id(), err) 1099 } 1100 inst.floatingIP = publicIP 1101 logger.Infof("assigned public IP %s to %q", publicIP.IP, inst.Id()) 1102 } 1103 if multiwatcher.AnyJobNeedsState(args.InstanceConfig.Jobs...) { 1104 if err := common.AddStateInstance(e.Storage(), inst.Id()); err != nil { 1105 logger.Errorf("could not record instance in provider-state: %v", err) 1106 } 1107 } 1108 return &environs.StartInstanceResult{ 1109 Instance: inst, 1110 Hardware: inst.hardwareCharacteristics(), 1111 }, nil 1112 } 1113 1114 func isNoValidHostsError(err error) bool { 1115 gooseErr, ok := err.(gooseerrors.Error) 1116 return ok && strings.Contains(gooseErr.Cause().Error(), "No valid host was found") 1117 } 1118 1119 func (e *environ) StopInstances(ids ...instance.Id) error { 1120 // If in instance firewall mode, gather the security group names. 1121 var securityGroupNames []string 1122 if e.Config().FirewallMode() == config.FwInstance { 1123 instances, err := e.Instances(ids) 1124 if err == environs.ErrNoInstances { 1125 return nil 1126 } 1127 securityGroupNames = make([]string, 0, len(ids)) 1128 for _, inst := range instances { 1129 if inst == nil { 1130 continue 1131 } 1132 openstackName := inst.(*openstackInstance).getServerDetail().Name 1133 lastDashPos := strings.LastIndex(openstackName, "-") 1134 if lastDashPos == -1 { 1135 return fmt.Errorf("cannot identify machine ID in openstack server name %q", openstackName) 1136 } 1137 securityGroupName := e.machineGroupName(openstackName[lastDashPos+1:]) 1138 securityGroupNames = append(securityGroupNames, securityGroupName) 1139 } 1140 } 1141 logger.Debugf("terminating instances %v", ids) 1142 if err := e.terminateInstances(ids); err != nil { 1143 return err 1144 } 1145 if securityGroupNames != nil { 1146 return e.deleteSecurityGroups(securityGroupNames) 1147 } 1148 return common.RemoveStateInstances(e.Storage(), ids...) 1149 } 1150 1151 func (e *environ) isAliveServer(server nova.ServerDetail) bool { 1152 switch server.Status { 1153 // HPCloud uses "BUILD(spawning)" as an intermediate BUILD state 1154 // once networking is available. 1155 case nova.StatusActive, nova.StatusBuild, nova.StatusBuildSpawning, nova.StatusShutoff, nova.StatusSuspended: 1156 return true 1157 } 1158 return false 1159 } 1160 1161 func (e *environ) listServers(ids []instance.Id) ([]nova.ServerDetail, error) { 1162 wantedServers := make([]nova.ServerDetail, 0, len(ids)) 1163 if len(ids) == 1 { 1164 // Common case, single instance, may return NotFound 1165 var maybeServer *nova.ServerDetail 1166 maybeServer, err := e.nova().GetServer(string(ids[0])) 1167 if err != nil { 1168 return nil, err 1169 } 1170 // Only return server details if it is currently alive 1171 if maybeServer != nil && e.isAliveServer(*maybeServer) { 1172 wantedServers = append(wantedServers, *maybeServer) 1173 } 1174 return wantedServers, nil 1175 } 1176 // List all servers that may be in the environment 1177 servers, err := e.nova().ListServersDetail(e.machinesFilter()) 1178 if err != nil { 1179 return nil, err 1180 } 1181 // Create a set of the ids of servers that are wanted 1182 idSet := make(map[string]struct{}, len(ids)) 1183 for _, id := range ids { 1184 idSet[string(id)] = struct{}{} 1185 } 1186 // Return only servers with the wanted ids that are currently alive 1187 for _, server := range servers { 1188 if _, ok := idSet[server.Id]; ok && e.isAliveServer(server) { 1189 wantedServers = append(wantedServers, server) 1190 } 1191 } 1192 return wantedServers, nil 1193 } 1194 1195 // updateFloatingIPAddresses updates the instances with any floating IP address 1196 // that have been assigned to those instances. 1197 func (e *environ) updateFloatingIPAddresses(instances map[string]instance.Instance) error { 1198 fips, err := e.nova().ListFloatingIPs() 1199 if err != nil { 1200 return err 1201 } 1202 for _, fip := range fips { 1203 if fip.InstanceId != nil && *fip.InstanceId != "" { 1204 instId := *fip.InstanceId 1205 if inst, ok := instances[instId]; ok { 1206 instFip := fip 1207 inst.(*openstackInstance).floatingIP = &instFip 1208 } 1209 } 1210 } 1211 return nil 1212 } 1213 1214 func (e *environ) Instances(ids []instance.Id) ([]instance.Instance, error) { 1215 if len(ids) == 0 { 1216 return nil, nil 1217 } 1218 // Make a series of requests to cope with eventual consistency. 1219 // Each request will attempt to add more instances to the requested 1220 // set. 1221 var foundServers []nova.ServerDetail 1222 for a := shortAttempt.Start(); a.Next(); { 1223 var err error 1224 foundServers, err = e.listServers(ids) 1225 if err != nil { 1226 logger.Debugf("error listing servers: %v", err) 1227 if !gooseerrors.IsNotFound(err) { 1228 return nil, err 1229 } 1230 } 1231 if len(foundServers) == len(ids) { 1232 break 1233 } 1234 } 1235 logger.Tracef("%d/%d live servers found", len(foundServers), len(ids)) 1236 if len(foundServers) == 0 { 1237 return nil, environs.ErrNoInstances 1238 } 1239 1240 instsById := make(map[string]instance.Instance, len(foundServers)) 1241 for i, server := range foundServers { 1242 // TODO(wallyworld): lookup the flavor details to fill in the 1243 // instance type data 1244 instsById[server.Id] = &openstackInstance{ 1245 e: e, 1246 serverDetail: &foundServers[i], 1247 } 1248 } 1249 1250 // Update the instance structs with any floating IP address that has been assigned to the instance. 1251 if e.ecfg().useFloatingIP() { 1252 if err := e.updateFloatingIPAddresses(instsById); err != nil { 1253 return nil, err 1254 } 1255 } 1256 1257 insts := make([]instance.Instance, len(ids)) 1258 var err error 1259 for i, id := range ids { 1260 if inst := instsById[string(id)]; inst != nil { 1261 insts[i] = inst 1262 } else { 1263 err = environs.ErrPartialInstances 1264 } 1265 } 1266 return insts, err 1267 } 1268 1269 func (e *environ) AllInstances() (insts []instance.Instance, err error) { 1270 servers, err := e.nova().ListServersDetail(e.machinesFilter()) 1271 if err != nil { 1272 return nil, err 1273 } 1274 instsById := make(map[string]instance.Instance) 1275 for _, server := range servers { 1276 if e.isAliveServer(server) { 1277 var s = server 1278 // TODO(wallyworld): lookup the flavor details to fill in the instance type data 1279 instsById[s.Id] = &openstackInstance{e: e, serverDetail: &s} 1280 } 1281 } 1282 1283 if e.ecfg().useFloatingIP() { 1284 if err := e.updateFloatingIPAddresses(instsById); err != nil { 1285 return nil, err 1286 } 1287 } 1288 1289 for _, inst := range instsById { 1290 insts = append(insts, inst) 1291 } 1292 return insts, err 1293 } 1294 1295 func (e *environ) Destroy() error { 1296 err := common.Destroy(e) 1297 if err != nil { 1298 return errors.Trace(err) 1299 } 1300 if err := e.Storage().RemoveAll(); err != nil { 1301 return errors.Trace(err) 1302 } 1303 novaClient := e.nova() 1304 securityGroups, err := novaClient.ListSecurityGroups() 1305 if err != nil { 1306 return errors.Annotate(err, "cannot list security groups") 1307 } 1308 re, err := regexp.Compile(fmt.Sprintf("^%s(-\\d+)?$", e.jujuGroupName())) 1309 if err != nil { 1310 return errors.Trace(err) 1311 } 1312 globalGroupName := e.globalGroupName() 1313 for _, group := range securityGroups { 1314 if re.MatchString(group.Name) || group.Name == globalGroupName { 1315 err = novaClient.DeleteSecurityGroup(group.Id) 1316 if err != nil { 1317 logger.Warningf("cannot delete security group %q. Used by another environment?", group.Name) 1318 } 1319 } 1320 } 1321 return nil 1322 } 1323 1324 func (e *environ) globalGroupName() string { 1325 return fmt.Sprintf("%s-global", e.jujuGroupName()) 1326 } 1327 1328 func (e *environ) machineGroupName(machineId string) string { 1329 return fmt.Sprintf("%s-%s", e.jujuGroupName(), machineId) 1330 } 1331 1332 func (e *environ) jujuGroupName() string { 1333 return fmt.Sprintf("juju-%s", e.name) 1334 } 1335 1336 func resourceName(tag names.Tag, envName string) string { 1337 return fmt.Sprintf("juju-%s-%s", envName, tag) 1338 } 1339 1340 // machinesFilter returns a nova.Filter matching all machines in the environment. 1341 func (e *environ) machinesFilter() *nova.Filter { 1342 filter := nova.NewFilter() 1343 filter.Set(nova.FilterServer, fmt.Sprintf("juju-%s-machine-\\d*", e.Config().Name())) 1344 return filter 1345 } 1346 1347 // portsToRuleInfo maps port ranges to nova rules 1348 func portsToRuleInfo(groupId string, ports []network.PortRange) []nova.RuleInfo { 1349 rules := make([]nova.RuleInfo, len(ports)) 1350 for i, portRange := range ports { 1351 rules[i] = nova.RuleInfo{ 1352 ParentGroupId: groupId, 1353 FromPort: portRange.FromPort, 1354 ToPort: portRange.ToPort, 1355 IPProtocol: portRange.Protocol, 1356 Cidr: "0.0.0.0/0", 1357 } 1358 } 1359 return rules 1360 } 1361 1362 func (e *environ) openPortsInGroup(name string, portRanges []network.PortRange) error { 1363 novaclient := e.nova() 1364 group, err := novaclient.SecurityGroupByName(name) 1365 if err != nil { 1366 return err 1367 } 1368 rules := portsToRuleInfo(group.Id, portRanges) 1369 for _, rule := range rules { 1370 _, err := novaclient.CreateSecurityGroupRule(rule) 1371 if err != nil { 1372 // TODO: if err is not rule already exists, raise? 1373 logger.Debugf("error creating security group rule: %v", err.Error()) 1374 } 1375 } 1376 return nil 1377 } 1378 1379 // ruleMatchesPortRange checks if supplied nova security group rule matches the port range 1380 func ruleMatchesPortRange(rule nova.SecurityGroupRule, portRange network.PortRange) bool { 1381 if rule.IPProtocol == nil || rule.FromPort == nil || rule.ToPort == nil { 1382 return false 1383 } 1384 return *rule.IPProtocol == portRange.Protocol && 1385 *rule.FromPort == portRange.FromPort && 1386 *rule.ToPort == portRange.ToPort 1387 } 1388 1389 func (e *environ) closePortsInGroup(name string, portRanges []network.PortRange) error { 1390 if len(portRanges) == 0 { 1391 return nil 1392 } 1393 novaclient := e.nova() 1394 group, err := novaclient.SecurityGroupByName(name) 1395 if err != nil { 1396 return err 1397 } 1398 // TODO: Hey look ma, it's quadratic 1399 for _, portRange := range portRanges { 1400 for _, p := range (*group).Rules { 1401 if !ruleMatchesPortRange(p, portRange) { 1402 continue 1403 } 1404 err := novaclient.DeleteSecurityGroupRule(p.Id) 1405 if err != nil { 1406 return err 1407 } 1408 break 1409 } 1410 } 1411 return nil 1412 } 1413 1414 func (e *environ) portsInGroup(name string) (portRanges []network.PortRange, err error) { 1415 group, err := e.nova().SecurityGroupByName(name) 1416 if err != nil { 1417 return nil, err 1418 } 1419 for _, p := range (*group).Rules { 1420 portRanges = append(portRanges, network.PortRange{ 1421 Protocol: *p.IPProtocol, 1422 FromPort: *p.FromPort, 1423 ToPort: *p.ToPort, 1424 }) 1425 } 1426 network.SortPortRanges(portRanges) 1427 return portRanges, nil 1428 } 1429 1430 // TODO: following 30 lines nearly verbatim from environs/ec2 1431 1432 func (e *environ) OpenPorts(ports []network.PortRange) error { 1433 if e.Config().FirewallMode() != config.FwGlobal { 1434 return fmt.Errorf("invalid firewall mode %q for opening ports on environment", 1435 e.Config().FirewallMode()) 1436 } 1437 if err := e.openPortsInGroup(e.globalGroupName(), ports); err != nil { 1438 return err 1439 } 1440 logger.Infof("opened ports in global group: %v", ports) 1441 return nil 1442 } 1443 1444 func (e *environ) ClosePorts(ports []network.PortRange) error { 1445 if e.Config().FirewallMode() != config.FwGlobal { 1446 return fmt.Errorf("invalid firewall mode %q for closing ports on environment", 1447 e.Config().FirewallMode()) 1448 } 1449 if err := e.closePortsInGroup(e.globalGroupName(), ports); err != nil { 1450 return err 1451 } 1452 logger.Infof("closed ports in global group: %v", ports) 1453 return nil 1454 } 1455 1456 func (e *environ) Ports() ([]network.PortRange, error) { 1457 if e.Config().FirewallMode() != config.FwGlobal { 1458 return nil, fmt.Errorf("invalid firewall mode %q for retrieving ports from environment", 1459 e.Config().FirewallMode()) 1460 } 1461 return e.portsInGroup(e.globalGroupName()) 1462 } 1463 1464 func (e *environ) Provider() environs.EnvironProvider { 1465 return &providerInstance 1466 } 1467 1468 func (e *environ) setUpGlobalGroup(groupName string, apiPort int) (nova.SecurityGroup, error) { 1469 return e.ensureGroup(groupName, 1470 []nova.RuleInfo{ 1471 { 1472 IPProtocol: "tcp", 1473 FromPort: 22, 1474 ToPort: 22, 1475 Cidr: "0.0.0.0/0", 1476 }, 1477 { 1478 IPProtocol: "tcp", 1479 FromPort: apiPort, 1480 ToPort: apiPort, 1481 Cidr: "0.0.0.0/0", 1482 }, 1483 { 1484 IPProtocol: "tcp", 1485 FromPort: 1, 1486 ToPort: 65535, 1487 }, 1488 { 1489 IPProtocol: "udp", 1490 FromPort: 1, 1491 ToPort: 65535, 1492 }, 1493 { 1494 IPProtocol: "icmp", 1495 FromPort: -1, 1496 ToPort: -1, 1497 }, 1498 }) 1499 } 1500 1501 // setUpGroups creates the security groups for the new machine, and 1502 // returns them. 1503 // 1504 // Instances are tagged with a group so they can be distinguished from 1505 // other instances that might be running on the same OpenStack account. 1506 // In addition, a specific machine security group is created for each 1507 // machine, so that its firewall rules can be configured per machine. 1508 // 1509 // Note: ideally we'd have a better way to determine group membership so that 2 1510 // people that happen to share an openstack account and name their environment 1511 // "openstack" don't end up destroying each other's machines. 1512 func (e *environ) setUpGroups(machineId string, apiPort int) ([]nova.SecurityGroup, error) { 1513 jujuGroup, err := e.setUpGlobalGroup(e.jujuGroupName(), apiPort) 1514 if err != nil { 1515 return nil, err 1516 } 1517 var machineGroup nova.SecurityGroup 1518 switch e.Config().FirewallMode() { 1519 case config.FwInstance: 1520 machineGroup, err = e.ensureGroup(e.machineGroupName(machineId), nil) 1521 case config.FwGlobal: 1522 machineGroup, err = e.ensureGroup(e.globalGroupName(), nil) 1523 } 1524 if err != nil { 1525 return nil, err 1526 } 1527 groups := []nova.SecurityGroup{jujuGroup, machineGroup} 1528 if e.ecfg().useDefaultSecurityGroup() { 1529 defaultGroup, err := e.nova().SecurityGroupByName("default") 1530 if err != nil { 1531 return nil, fmt.Errorf("loading default security group: %v", err) 1532 } 1533 groups = append(groups, *defaultGroup) 1534 } 1535 return groups, nil 1536 } 1537 1538 // zeroGroup holds the zero security group. 1539 var zeroGroup nova.SecurityGroup 1540 1541 // ensureGroup returns the security group with name and perms. 1542 // If a group with name does not exist, one will be created. 1543 // If it exists, its permissions are set to perms. 1544 func (e *environ) ensureGroup(name string, rules []nova.RuleInfo) (nova.SecurityGroup, error) { 1545 novaClient := e.nova() 1546 // First attempt to look up an existing group by name. 1547 group, err := novaClient.SecurityGroupByName(name) 1548 if err == nil { 1549 // Group exists, so assume it is correctly set up and return it. 1550 // TODO(jam): 2013-09-18 http://pad.lv/121795 1551 // We really should verify the group is set up correctly, 1552 // because deleting and re-creating environments can get us bad 1553 // groups (especially if they were set up under Python) 1554 return *group, nil 1555 } 1556 // Doesn't exist, so try and create it. 1557 group, err = novaClient.CreateSecurityGroup(name, "juju group") 1558 if err != nil { 1559 if !gooseerrors.IsDuplicateValue(err) { 1560 return zeroGroup, err 1561 } else { 1562 // We just tried to create a duplicate group, so load the existing group. 1563 group, err = novaClient.SecurityGroupByName(name) 1564 if err != nil { 1565 return zeroGroup, err 1566 } 1567 return *group, nil 1568 } 1569 } 1570 // The new group is created so now add the rules. 1571 group.Rules = make([]nova.SecurityGroupRule, len(rules)) 1572 for i, rule := range rules { 1573 rule.ParentGroupId = group.Id 1574 if rule.Cidr == "" { 1575 // http://pad.lv/1226996 Rules that don't have a CIDR 1576 // are meant to apply only to this group. If you don't 1577 // supply CIDR or GroupId then openstack assumes you 1578 // mean CIDR=0.0.0.0/0 1579 rule.GroupId = &group.Id 1580 } 1581 groupRule, err := novaClient.CreateSecurityGroupRule(rule) 1582 if err != nil && !gooseerrors.IsDuplicateValue(err) { 1583 return zeroGroup, err 1584 } 1585 group.Rules[i] = *groupRule 1586 } 1587 return *group, nil 1588 } 1589 1590 // deleteSecurityGroups deletes the given security groups. If a security 1591 // group is also used by another environment (see bug #1300755), an attempt 1592 // to delete this group fails. A warning is logged in this case. 1593 func (e *environ) deleteSecurityGroups(securityGroupNames []string) error { 1594 novaclient := e.nova() 1595 allSecurityGroups, err := novaclient.ListSecurityGroups() 1596 if err != nil { 1597 return err 1598 } 1599 for _, securityGroup := range allSecurityGroups { 1600 for _, name := range securityGroupNames { 1601 if securityGroup.Name == name { 1602 err := novaclient.DeleteSecurityGroup(securityGroup.Id) 1603 if err != nil { 1604 logger.Warningf("cannot delete security group %q. Used by another environment?", name) 1605 } 1606 break 1607 } 1608 } 1609 } 1610 return nil 1611 } 1612 1613 func (e *environ) terminateInstances(ids []instance.Id) error { 1614 if len(ids) == 0 { 1615 return nil 1616 } 1617 var firstErr error 1618 novaClient := e.nova() 1619 for _, id := range ids { 1620 err := novaClient.DeleteServer(string(id)) 1621 if gooseerrors.IsNotFound(err) { 1622 err = nil 1623 } 1624 if err != nil && firstErr == nil { 1625 logger.Debugf("error terminating instance %q: %v", id, err) 1626 firstErr = err 1627 } 1628 } 1629 return firstErr 1630 } 1631 1632 // MetadataLookupParams returns parameters which are used to query simplestreams metadata. 1633 func (e *environ) MetadataLookupParams(region string) (*simplestreams.MetadataLookupParams, error) { 1634 if region == "" { 1635 region = e.ecfg().region() 1636 } 1637 cloudSpec, err := e.cloudSpec(region) 1638 if err != nil { 1639 return nil, err 1640 } 1641 return &simplestreams.MetadataLookupParams{ 1642 Series: config.PreferredSeries(e.ecfg()), 1643 Region: cloudSpec.Region, 1644 Endpoint: cloudSpec.Endpoint, 1645 Architectures: arch.AllSupportedArches, 1646 }, nil 1647 } 1648 1649 // Region is specified in the HasRegion interface. 1650 func (e *environ) Region() (simplestreams.CloudSpec, error) { 1651 return e.cloudSpec(e.ecfg().region()) 1652 } 1653 1654 func (e *environ) cloudSpec(region string) (simplestreams.CloudSpec, error) { 1655 return simplestreams.CloudSpec{ 1656 Region: region, 1657 Endpoint: e.ecfg().authURL(), 1658 }, nil 1659 }