github.com/mhilton/juju-juju@v0.0.0-20150901100907-a94dd2c73455/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/environs/tags" 37 "github.com/juju/juju/instance" 38 "github.com/juju/juju/juju/arch" 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) 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 err = novaClient.DeleteSecurityGroup(group.Id) 1327 if err != nil { 1328 logger.Warningf("cannot delete security group %q. Used by another environment?", group.Name) 1329 } 1330 } 1331 } 1332 return nil 1333 } 1334 1335 func (e *environ) globalGroupName() string { 1336 return fmt.Sprintf("%s-global", e.jujuGroupName()) 1337 } 1338 1339 func (e *environ) machineGroupName(machineId string) string { 1340 return fmt.Sprintf("%s-%s", e.jujuGroupName(), machineId) 1341 } 1342 1343 func (e *environ) jujuGroupName() string { 1344 return fmt.Sprintf("juju-%s", e.name) 1345 } 1346 1347 func resourceName(tag names.Tag, envName string) string { 1348 return fmt.Sprintf("juju-%s-%s", envName, tag) 1349 } 1350 1351 // machinesFilter returns a nova.Filter matching all machines in the environment. 1352 func (e *environ) machinesFilter() *nova.Filter { 1353 filter := nova.NewFilter() 1354 filter.Set(nova.FilterServer, fmt.Sprintf("juju-%s-machine-\\d*", e.Config().Name())) 1355 return filter 1356 } 1357 1358 // portsToRuleInfo maps port ranges to nova rules 1359 func portsToRuleInfo(groupId string, ports []network.PortRange) []nova.RuleInfo { 1360 rules := make([]nova.RuleInfo, len(ports)) 1361 for i, portRange := range ports { 1362 rules[i] = nova.RuleInfo{ 1363 ParentGroupId: groupId, 1364 FromPort: portRange.FromPort, 1365 ToPort: portRange.ToPort, 1366 IPProtocol: portRange.Protocol, 1367 Cidr: "0.0.0.0/0", 1368 } 1369 } 1370 return rules 1371 } 1372 1373 func (e *environ) openPortsInGroup(name string, portRanges []network.PortRange) error { 1374 novaclient := e.nova() 1375 group, err := novaclient.SecurityGroupByName(name) 1376 if err != nil { 1377 return err 1378 } 1379 rules := portsToRuleInfo(group.Id, portRanges) 1380 for _, rule := range rules { 1381 _, err := novaclient.CreateSecurityGroupRule(rule) 1382 if err != nil { 1383 // TODO: if err is not rule already exists, raise? 1384 logger.Debugf("error creating security group rule: %v", err.Error()) 1385 } 1386 } 1387 return nil 1388 } 1389 1390 // ruleMatchesPortRange checks if supplied nova security group rule matches the port range 1391 func ruleMatchesPortRange(rule nova.SecurityGroupRule, portRange network.PortRange) bool { 1392 if rule.IPProtocol == nil || rule.FromPort == nil || rule.ToPort == nil { 1393 return false 1394 } 1395 return *rule.IPProtocol == portRange.Protocol && 1396 *rule.FromPort == portRange.FromPort && 1397 *rule.ToPort == portRange.ToPort 1398 } 1399 1400 func (e *environ) closePortsInGroup(name string, portRanges []network.PortRange) error { 1401 if len(portRanges) == 0 { 1402 return nil 1403 } 1404 novaclient := e.nova() 1405 group, err := novaclient.SecurityGroupByName(name) 1406 if err != nil { 1407 return err 1408 } 1409 // TODO: Hey look ma, it's quadratic 1410 for _, portRange := range portRanges { 1411 for _, p := range (*group).Rules { 1412 if !ruleMatchesPortRange(p, portRange) { 1413 continue 1414 } 1415 err := novaclient.DeleteSecurityGroupRule(p.Id) 1416 if err != nil { 1417 return err 1418 } 1419 break 1420 } 1421 } 1422 return nil 1423 } 1424 1425 func (e *environ) portsInGroup(name string) (portRanges []network.PortRange, err error) { 1426 group, err := e.nova().SecurityGroupByName(name) 1427 if err != nil { 1428 return nil, err 1429 } 1430 for _, p := range (*group).Rules { 1431 portRanges = append(portRanges, network.PortRange{ 1432 Protocol: *p.IPProtocol, 1433 FromPort: *p.FromPort, 1434 ToPort: *p.ToPort, 1435 }) 1436 } 1437 network.SortPortRanges(portRanges) 1438 return portRanges, nil 1439 } 1440 1441 // TODO: following 30 lines nearly verbatim from environs/ec2 1442 1443 func (e *environ) OpenPorts(ports []network.PortRange) error { 1444 if e.Config().FirewallMode() != config.FwGlobal { 1445 return fmt.Errorf("invalid firewall mode %q for opening ports on environment", 1446 e.Config().FirewallMode()) 1447 } 1448 if err := e.openPortsInGroup(e.globalGroupName(), ports); err != nil { 1449 return err 1450 } 1451 logger.Infof("opened ports in global group: %v", ports) 1452 return nil 1453 } 1454 1455 func (e *environ) ClosePorts(ports []network.PortRange) error { 1456 if e.Config().FirewallMode() != config.FwGlobal { 1457 return fmt.Errorf("invalid firewall mode %q for closing ports on environment", 1458 e.Config().FirewallMode()) 1459 } 1460 if err := e.closePortsInGroup(e.globalGroupName(), ports); err != nil { 1461 return err 1462 } 1463 logger.Infof("closed ports in global group: %v", ports) 1464 return nil 1465 } 1466 1467 func (e *environ) Ports() ([]network.PortRange, error) { 1468 if e.Config().FirewallMode() != config.FwGlobal { 1469 return nil, fmt.Errorf("invalid firewall mode %q for retrieving ports from environment", 1470 e.Config().FirewallMode()) 1471 } 1472 return e.portsInGroup(e.globalGroupName()) 1473 } 1474 1475 func (e *environ) Provider() environs.EnvironProvider { 1476 return &providerInstance 1477 } 1478 1479 func (e *environ) setUpGlobalGroup(groupName string, apiPort int) (nova.SecurityGroup, error) { 1480 return e.ensureGroup(groupName, 1481 []nova.RuleInfo{ 1482 { 1483 IPProtocol: "tcp", 1484 FromPort: 22, 1485 ToPort: 22, 1486 Cidr: "0.0.0.0/0", 1487 }, 1488 { 1489 IPProtocol: "tcp", 1490 FromPort: apiPort, 1491 ToPort: apiPort, 1492 Cidr: "0.0.0.0/0", 1493 }, 1494 { 1495 IPProtocol: "tcp", 1496 FromPort: 1, 1497 ToPort: 65535, 1498 }, 1499 { 1500 IPProtocol: "udp", 1501 FromPort: 1, 1502 ToPort: 65535, 1503 }, 1504 { 1505 IPProtocol: "icmp", 1506 FromPort: -1, 1507 ToPort: -1, 1508 }, 1509 }) 1510 } 1511 1512 // setUpGroups creates the security groups for the new machine, and 1513 // returns them. 1514 // 1515 // Instances are tagged with a group so they can be distinguished from 1516 // other instances that might be running on the same OpenStack account. 1517 // In addition, a specific machine security group is created for each 1518 // machine, so that its firewall rules can be configured per machine. 1519 // 1520 // Note: ideally we'd have a better way to determine group membership so that 2 1521 // people that happen to share an openstack account and name their environment 1522 // "openstack" don't end up destroying each other's machines. 1523 func (e *environ) setUpGroups(machineId string, apiPort int) ([]nova.SecurityGroup, error) { 1524 jujuGroup, err := e.setUpGlobalGroup(e.jujuGroupName(), apiPort) 1525 if err != nil { 1526 return nil, err 1527 } 1528 var machineGroup nova.SecurityGroup 1529 switch e.Config().FirewallMode() { 1530 case config.FwInstance: 1531 machineGroup, err = e.ensureGroup(e.machineGroupName(machineId), nil) 1532 case config.FwGlobal: 1533 machineGroup, err = e.ensureGroup(e.globalGroupName(), nil) 1534 } 1535 if err != nil { 1536 return nil, err 1537 } 1538 groups := []nova.SecurityGroup{jujuGroup, machineGroup} 1539 if e.ecfg().useDefaultSecurityGroup() { 1540 defaultGroup, err := e.nova().SecurityGroupByName("default") 1541 if err != nil { 1542 return nil, fmt.Errorf("loading default security group: %v", err) 1543 } 1544 groups = append(groups, *defaultGroup) 1545 } 1546 return groups, nil 1547 } 1548 1549 // zeroGroup holds the zero security group. 1550 var zeroGroup nova.SecurityGroup 1551 1552 // ensureGroup returns the security group with name and perms. 1553 // If a group with name does not exist, one will be created. 1554 // If it exists, its permissions are set to perms. 1555 func (e *environ) ensureGroup(name string, rules []nova.RuleInfo) (nova.SecurityGroup, error) { 1556 novaClient := e.nova() 1557 // First attempt to look up an existing group by name. 1558 group, err := novaClient.SecurityGroupByName(name) 1559 if err == nil { 1560 // Group exists, so assume it is correctly set up and return it. 1561 // TODO(jam): 2013-09-18 http://pad.lv/121795 1562 // We really should verify the group is set up correctly, 1563 // because deleting and re-creating environments can get us bad 1564 // groups (especially if they were set up under Python) 1565 return *group, nil 1566 } 1567 // Doesn't exist, so try and create it. 1568 group, err = novaClient.CreateSecurityGroup(name, "juju group") 1569 if err != nil { 1570 if !gooseerrors.IsDuplicateValue(err) { 1571 return zeroGroup, err 1572 } else { 1573 // We just tried to create a duplicate group, so load the existing group. 1574 group, err = novaClient.SecurityGroupByName(name) 1575 if err != nil { 1576 return zeroGroup, err 1577 } 1578 return *group, nil 1579 } 1580 } 1581 // The new group is created so now add the rules. 1582 group.Rules = make([]nova.SecurityGroupRule, len(rules)) 1583 for i, rule := range rules { 1584 rule.ParentGroupId = group.Id 1585 if rule.Cidr == "" { 1586 // http://pad.lv/1226996 Rules that don't have a CIDR 1587 // are meant to apply only to this group. If you don't 1588 // supply CIDR or GroupId then openstack assumes you 1589 // mean CIDR=0.0.0.0/0 1590 rule.GroupId = &group.Id 1591 } 1592 groupRule, err := novaClient.CreateSecurityGroupRule(rule) 1593 if err != nil && !gooseerrors.IsDuplicateValue(err) { 1594 return zeroGroup, err 1595 } 1596 group.Rules[i] = *groupRule 1597 } 1598 return *group, nil 1599 } 1600 1601 // deleteSecurityGroups deletes the given security groups. If a security 1602 // group is also used by another environment (see bug #1300755), an attempt 1603 // to delete this group fails. A warning is logged in this case. 1604 func (e *environ) deleteSecurityGroups(securityGroupNames []string) error { 1605 novaclient := e.nova() 1606 allSecurityGroups, err := novaclient.ListSecurityGroups() 1607 if err != nil { 1608 return err 1609 } 1610 for _, securityGroup := range allSecurityGroups { 1611 for _, name := range securityGroupNames { 1612 if securityGroup.Name == name { 1613 err := novaclient.DeleteSecurityGroup(securityGroup.Id) 1614 if err != nil { 1615 logger.Warningf("cannot delete security group %q. Used by another environment?", name) 1616 } 1617 break 1618 } 1619 } 1620 } 1621 return nil 1622 } 1623 1624 func (e *environ) terminateInstances(ids []instance.Id) error { 1625 if len(ids) == 0 { 1626 return nil 1627 } 1628 var firstErr error 1629 novaClient := e.nova() 1630 for _, id := range ids { 1631 err := novaClient.DeleteServer(string(id)) 1632 if gooseerrors.IsNotFound(err) { 1633 err = nil 1634 } 1635 if err != nil && firstErr == nil { 1636 logger.Debugf("error terminating instance %q: %v", id, err) 1637 firstErr = err 1638 } 1639 } 1640 return firstErr 1641 } 1642 1643 // MetadataLookupParams returns parameters which are used to query simplestreams metadata. 1644 func (e *environ) MetadataLookupParams(region string) (*simplestreams.MetadataLookupParams, error) { 1645 if region == "" { 1646 region = e.ecfg().region() 1647 } 1648 cloudSpec, err := e.cloudSpec(region) 1649 if err != nil { 1650 return nil, err 1651 } 1652 return &simplestreams.MetadataLookupParams{ 1653 Series: config.PreferredSeries(e.ecfg()), 1654 Region: cloudSpec.Region, 1655 Endpoint: cloudSpec.Endpoint, 1656 Architectures: arch.AllSupportedArches, 1657 }, nil 1658 } 1659 1660 // Region is specified in the HasRegion interface. 1661 func (e *environ) Region() (simplestreams.CloudSpec, error) { 1662 return e.cloudSpec(e.ecfg().region()) 1663 } 1664 1665 func (e *environ) cloudSpec(region string) (simplestreams.CloudSpec, error) { 1666 return simplestreams.CloudSpec{ 1667 Region: region, 1668 Endpoint: e.ecfg().authURL(), 1669 }, nil 1670 } 1671 1672 // TagInstance implements environs.InstanceTagger. 1673 func (e *environ) TagInstance(id instance.Id, tags map[string]string) error { 1674 if err := e.nova().SetServerMetadata(string(id), tags); err != nil { 1675 return errors.Annotate(err, "setting server metadata") 1676 } 1677 return nil 1678 }