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