github.com/wallyworld/juju@v0.0.0-20161013125918-6cf1bc9d917a/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 "log" 11 "net/url" 12 "strings" 13 "sync" 14 "time" 15 16 "github.com/juju/errors" 17 "github.com/juju/loggo" 18 "github.com/juju/utils" 19 "github.com/juju/version" 20 "gopkg.in/goose.v1/cinder" 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 26 "github.com/juju/juju/cloud" 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/instances" 33 "github.com/juju/juju/environs/simplestreams" 34 "github.com/juju/juju/environs/tags" 35 "github.com/juju/juju/instance" 36 "github.com/juju/juju/network" 37 "github.com/juju/juju/provider/common" 38 "github.com/juju/juju/state" 39 "github.com/juju/juju/status" 40 "github.com/juju/juju/tools" 41 ) 42 43 var logger = loggo.GetLogger("juju.provider.openstack") 44 45 type EnvironProvider struct { 46 environs.ProviderCredentials 47 Configurator ProviderConfigurator 48 FirewallerFactory FirewallerFactory 49 } 50 51 var ( 52 _ environs.EnvironProvider = (*EnvironProvider)(nil) 53 _ environs.ProviderSchema = (*EnvironProvider)(nil) 54 ) 55 56 var providerInstance *EnvironProvider = &EnvironProvider{ 57 OpenstackCredentials{}, 58 &defaultConfigurator{}, 59 &firewallerFactory{}, 60 } 61 62 var makeServiceURL = client.AuthenticatingClient.MakeServiceURL 63 64 // TODO: shortAttempt was kept to a long timeout because Nova needs 65 // more time than EC2. Storage delays are handled separately now, and 66 // perhaps other polling attempts can time out faster. 67 68 // shortAttempt is used when polling for short-term events in tests. 69 var shortAttempt = utils.AttemptStrategy{ 70 Total: 15 * time.Second, 71 Delay: 200 * time.Millisecond, 72 } 73 74 func (p EnvironProvider) Open(args environs.OpenParams) (environs.Environ, error) { 75 logger.Infof("opening model %q", args.Config.Name()) 76 if err := validateCloudSpec(args.Cloud); err != nil { 77 return nil, errors.Annotate(err, "validating cloud spec") 78 } 79 uuid := args.Config.UUID() 80 namespace, err := instance.NewNamespace(uuid) 81 if err != nil { 82 return nil, errors.Annotate(err, "creating instance namespace") 83 } 84 85 e := &Environ{ 86 name: args.Config.Name(), 87 uuid: uuid, 88 cloud: args.Cloud, 89 namespace: namespace, 90 } 91 e.firewaller = p.FirewallerFactory.GetFirewaller(e) 92 e.configurator = p.Configurator 93 if err := e.SetConfig(args.Config); err != nil { 94 return nil, err 95 } 96 return e, nil 97 } 98 99 // DetectRegions implements environs.CloudRegionDetector. 100 func (EnvironProvider) DetectRegions() ([]cloud.Region, error) { 101 // If OS_REGION_NAME and OS_AUTH_URL are both set, 102 // return return a region using them. 103 creds := identity.CredentialsFromEnv() 104 if creds.Region == "" { 105 return nil, errors.NewNotFound(nil, "OS_REGION_NAME environment variable not set") 106 } 107 if creds.URL == "" { 108 return nil, errors.NewNotFound(nil, "OS_AUTH_URL environment variable not set") 109 } 110 return []cloud.Region{{ 111 Name: creds.Region, 112 Endpoint: creds.URL, 113 }}, nil 114 } 115 116 // PrepareConfig is specified in the EnvironProvider interface. 117 func (p EnvironProvider) PrepareConfig(args environs.PrepareConfigParams) (*config.Config, error) { 118 if err := validateCloudSpec(args.Cloud); err != nil { 119 return nil, errors.Annotate(err, "validating cloud spec") 120 } 121 122 // Set the default block-storage source. 123 attrs := make(map[string]interface{}) 124 if _, ok := args.Config.StorageDefaultBlockSource(); !ok { 125 attrs[config.StorageDefaultBlockSourceKey] = CinderProviderType 126 } 127 128 cfg, err := args.Config.Apply(attrs) 129 if err != nil { 130 return nil, errors.Trace(err) 131 } 132 return cfg, nil 133 } 134 135 // MetadataLookupParams returns parameters which are used to query image metadata to 136 // find matching image information. 137 func (p EnvironProvider) MetadataLookupParams(region string) (*simplestreams.MetadataLookupParams, error) { 138 if region == "" { 139 return nil, errors.Errorf("region must be specified") 140 } 141 return &simplestreams.MetadataLookupParams{ 142 Region: region, 143 }, nil 144 } 145 146 func (p EnvironProvider) newConfig(cfg *config.Config) (*environConfig, error) { 147 valid, err := p.Validate(cfg, nil) 148 if err != nil { 149 return nil, err 150 } 151 return &environConfig{valid, valid.UnknownAttrs()}, nil 152 } 153 154 type Environ struct { 155 name string 156 uuid string 157 cloud environs.CloudSpec 158 namespace instance.Namespace 159 160 ecfgMutex sync.Mutex 161 ecfgUnlocked *environConfig 162 client client.AuthenticatingClient 163 novaUnlocked *nova.Client 164 volumeURL *url.URL 165 166 // keystoneImageDataSource caches the result of getKeystoneImageSource. 167 keystoneImageDataSourceMutex sync.Mutex 168 keystoneImageDataSource simplestreams.DataSource 169 170 // keystoneToolsDataSource caches the result of getKeystoneToolsSource. 171 keystoneToolsDataSourceMutex sync.Mutex 172 keystoneToolsDataSource simplestreams.DataSource 173 174 availabilityZonesMutex sync.Mutex 175 availabilityZones []common.AvailabilityZone 176 firewaller Firewaller 177 configurator ProviderConfigurator 178 } 179 180 var _ environs.Environ = (*Environ)(nil) 181 var _ simplestreams.HasRegion = (*Environ)(nil) 182 var _ state.Prechecker = (*Environ)(nil) 183 var _ instance.Distributor = (*Environ)(nil) 184 var _ environs.InstanceTagger = (*Environ)(nil) 185 186 type openstackInstance struct { 187 e *Environ 188 instType *instances.InstanceType 189 arch *string 190 191 mu sync.Mutex 192 serverDetail *nova.ServerDetail 193 // floatingIP is non-nil iff use-floating-ip is true. 194 floatingIP *nova.FloatingIP 195 } 196 197 func (inst *openstackInstance) String() string { 198 return string(inst.Id()) 199 } 200 201 var _ instance.Instance = (*openstackInstance)(nil) 202 203 func (inst *openstackInstance) Refresh() error { 204 inst.mu.Lock() 205 defer inst.mu.Unlock() 206 server, err := inst.e.nova().GetServer(inst.serverDetail.Id) 207 if err != nil { 208 return err 209 } 210 inst.serverDetail = server 211 return nil 212 } 213 214 func (inst *openstackInstance) getServerDetail() *nova.ServerDetail { 215 inst.mu.Lock() 216 defer inst.mu.Unlock() 217 return inst.serverDetail 218 } 219 220 func (inst *openstackInstance) Id() instance.Id { 221 return instance.Id(inst.getServerDetail().Id) 222 } 223 224 func (inst *openstackInstance) Status() instance.InstanceStatus { 225 instStatus := inst.getServerDetail().Status 226 jujuStatus := status.Pending 227 switch instStatus { 228 case nova.StatusActive: 229 jujuStatus = status.Running 230 case nova.StatusError: 231 jujuStatus = status.ProvisioningError 232 case nova.StatusBuild, nova.StatusBuildSpawning, 233 nova.StatusDeleted, nova.StatusHardReboot, 234 nova.StatusPassword, nova.StatusReboot, 235 nova.StatusRebuild, nova.StatusRescue, 236 nova.StatusResize, nova.StatusShutoff, 237 nova.StatusSuspended, nova.StatusVerifyResize: 238 jujuStatus = status.Empty 239 case nova.StatusUnknown: 240 jujuStatus = status.Unknown 241 default: 242 jujuStatus = status.Empty 243 } 244 return instance.InstanceStatus{ 245 Status: jujuStatus, 246 Message: instStatus, 247 } 248 } 249 250 func (inst *openstackInstance) hardwareCharacteristics() *instance.HardwareCharacteristics { 251 hc := &instance.HardwareCharacteristics{Arch: inst.arch} 252 if inst.instType != nil { 253 hc.Mem = &inst.instType.Mem 254 // openstack is special in that a 0-size root disk means that 255 // the root disk will result in an instance with a root disk 256 // the same size as the image that created it, so we just set 257 // the HardwareCharacteristics to nil to signal that we don't 258 // know what the correct size is. 259 if inst.instType.RootDisk == 0 { 260 hc.RootDisk = nil 261 } else { 262 hc.RootDisk = &inst.instType.RootDisk 263 } 264 hc.CpuCores = &inst.instType.CpuCores 265 hc.CpuPower = inst.instType.CpuPower 266 // tags not currently supported on openstack 267 } 268 hc.AvailabilityZone = &inst.serverDetail.AvailabilityZone 269 return hc 270 } 271 272 // getAddresses returns the existing server information on addresses, 273 // but fetches the details over the api again if no addresses exist. 274 func (inst *openstackInstance) getAddresses() (map[string][]nova.IPAddress, error) { 275 addrs := inst.getServerDetail().Addresses 276 if len(addrs) == 0 { 277 server, err := inst.e.nova().GetServer(string(inst.Id())) 278 if err != nil { 279 return nil, err 280 } 281 addrs = server.Addresses 282 } 283 return addrs, nil 284 } 285 286 // Addresses implements network.Addresses() returning generic address 287 // details for the instances, and calling the openstack api if needed. 288 func (inst *openstackInstance) Addresses() ([]network.Address, error) { 289 addresses, err := inst.getAddresses() 290 if err != nil { 291 return nil, err 292 } 293 var floatingIP string 294 if inst.floatingIP != nil && inst.floatingIP.IP != "" { 295 floatingIP = inst.floatingIP.IP 296 logger.Debugf("instance %v has floating IP address: %v", inst.Id(), floatingIP) 297 } 298 return convertNovaAddresses(floatingIP, addresses), nil 299 } 300 301 // convertNovaAddresses returns nova addresses in generic format 302 func convertNovaAddresses(publicIP string, addresses map[string][]nova.IPAddress) []network.Address { 303 var machineAddresses []network.Address 304 if publicIP != "" { 305 publicAddr := network.NewScopedAddress(publicIP, network.ScopePublic) 306 machineAddresses = append(machineAddresses, publicAddr) 307 } 308 // TODO(gz) Network ordering may be significant but is not preserved by 309 // the map, see lp:1188126 for example. That could potentially be fixed 310 // in goose, or left to be derived by other means. 311 for netName, ips := range addresses { 312 networkScope := network.ScopeUnknown 313 if netName == "public" { 314 networkScope = network.ScopePublic 315 } 316 for _, address := range ips { 317 // If this address has already been added as a floating IP, skip it. 318 if publicIP == address.Address { 319 continue 320 } 321 // Assume IPv4 unless specified otherwise 322 addrtype := network.IPv4Address 323 if address.Version == 6 { 324 addrtype = network.IPv6Address 325 } 326 machineAddr := network.NewScopedAddress(address.Address, networkScope) 327 if machineAddr.Type != addrtype { 328 logger.Warningf("derived address type %v, nova reports %v", machineAddr.Type, addrtype) 329 } 330 machineAddresses = append(machineAddresses, machineAddr) 331 } 332 } 333 return machineAddresses 334 } 335 336 func (inst *openstackInstance) OpenPorts(machineId string, ports []network.PortRange) error { 337 return inst.e.firewaller.OpenInstancePorts(inst, machineId, ports) 338 } 339 340 func (inst *openstackInstance) ClosePorts(machineId string, ports []network.PortRange) error { 341 return inst.e.firewaller.CloseInstancePorts(inst, machineId, ports) 342 } 343 344 func (inst *openstackInstance) Ports(machineId string) ([]network.PortRange, error) { 345 return inst.e.firewaller.InstancePorts(inst, machineId) 346 } 347 348 func (e *Environ) ecfg() *environConfig { 349 e.ecfgMutex.Lock() 350 ecfg := e.ecfgUnlocked 351 e.ecfgMutex.Unlock() 352 return ecfg 353 } 354 355 func (e *Environ) nova() *nova.Client { 356 e.ecfgMutex.Lock() 357 nova := e.novaUnlocked 358 e.ecfgMutex.Unlock() 359 return nova 360 } 361 362 var unsupportedConstraints = []string{ 363 constraints.Tags, 364 constraints.CpuPower, 365 } 366 367 // ConstraintsValidator is defined on the Environs interface. 368 func (e *Environ) ConstraintsValidator() (constraints.Validator, error) { 369 validator := constraints.NewValidator() 370 validator.RegisterConflicts( 371 []string{constraints.InstanceType}, 372 []string{constraints.Mem, constraints.RootDisk, constraints.Cores}) 373 validator.RegisterUnsupported(unsupportedConstraints) 374 novaClient := e.nova() 375 flavors, err := novaClient.ListFlavorsDetail() 376 if err != nil { 377 return nil, err 378 } 379 instTypeNames := make([]string, len(flavors)) 380 for i, flavor := range flavors { 381 instTypeNames[i] = flavor.Name 382 } 383 validator.RegisterVocabulary(constraints.InstanceType, instTypeNames) 384 validator.RegisterVocabulary(constraints.VirtType, []string{"kvm", "lxd"}) 385 return validator, nil 386 } 387 388 var novaListAvailabilityZones = (*nova.Client).ListAvailabilityZones 389 390 type openstackAvailabilityZone struct { 391 nova.AvailabilityZone 392 } 393 394 func (z *openstackAvailabilityZone) Name() string { 395 return z.AvailabilityZone.Name 396 } 397 398 func (z *openstackAvailabilityZone) Available() bool { 399 return z.AvailabilityZone.State.Available 400 } 401 402 // AvailabilityZones returns a slice of availability zones. 403 func (e *Environ) AvailabilityZones() ([]common.AvailabilityZone, error) { 404 e.availabilityZonesMutex.Lock() 405 defer e.availabilityZonesMutex.Unlock() 406 if e.availabilityZones == nil { 407 zones, err := novaListAvailabilityZones(e.nova()) 408 if gooseerrors.IsNotImplemented(err) { 409 return nil, errors.NotImplementedf("availability zones") 410 } 411 if err != nil { 412 return nil, err 413 } 414 e.availabilityZones = make([]common.AvailabilityZone, len(zones)) 415 for i, z := range zones { 416 e.availabilityZones[i] = &openstackAvailabilityZone{z} 417 } 418 } 419 return e.availabilityZones, nil 420 } 421 422 // InstanceAvailabilityZoneNames returns the availability zone names for each 423 // of the specified instances. 424 func (e *Environ) InstanceAvailabilityZoneNames(ids []instance.Id) ([]string, error) { 425 instances, err := e.Instances(ids) 426 if err != nil && err != environs.ErrPartialInstances { 427 return nil, err 428 } 429 zones := make([]string, len(instances)) 430 for i, inst := range instances { 431 if inst == nil { 432 continue 433 } 434 zones[i] = inst.(*openstackInstance).serverDetail.AvailabilityZone 435 } 436 return zones, err 437 } 438 439 type openstackPlacement struct { 440 availabilityZone nova.AvailabilityZone 441 } 442 443 func (e *Environ) parsePlacement(placement string) (*openstackPlacement, error) { 444 pos := strings.IndexRune(placement, '=') 445 if pos == -1 { 446 return nil, errors.Errorf("unknown placement directive: %v", placement) 447 } 448 switch key, value := placement[:pos], placement[pos+1:]; key { 449 case "zone": 450 availabilityZone := value 451 zones, err := e.AvailabilityZones() 452 if err != nil { 453 return nil, err 454 } 455 for _, z := range zones { 456 if z.Name() == availabilityZone { 457 return &openstackPlacement{ 458 z.(*openstackAvailabilityZone).AvailabilityZone, 459 }, nil 460 } 461 } 462 return nil, errors.Errorf("invalid availability zone %q", availabilityZone) 463 } 464 return nil, errors.Errorf("unknown placement directive: %v", placement) 465 } 466 467 // PrecheckInstance is defined on the state.Prechecker interface. 468 func (e *Environ) PrecheckInstance(series string, cons constraints.Value, placement string) error { 469 if placement != "" { 470 if _, err := e.parsePlacement(placement); err != nil { 471 return err 472 } 473 } 474 if !cons.HasInstanceType() { 475 return nil 476 } 477 // Constraint has an instance-type constraint so let's see if it is valid. 478 novaClient := e.nova() 479 flavors, err := novaClient.ListFlavorsDetail() 480 if err != nil { 481 return err 482 } 483 for _, flavor := range flavors { 484 if flavor.Name == *cons.InstanceType { 485 return nil 486 } 487 } 488 return errors.Errorf("invalid Openstack flavour %q specified", *cons.InstanceType) 489 } 490 491 // PrepareForBootstrap is part of the Environ interface. 492 func (e *Environ) PrepareForBootstrap(ctx environs.BootstrapContext) error { 493 // Verify credentials. 494 if err := authenticateClient(e.client); err != nil { 495 return err 496 } 497 return nil 498 } 499 500 // Create is part of the Environ interface. 501 func (e *Environ) Create(environs.CreateParams) error { 502 // Verify credentials. 503 if err := authenticateClient(e.client); err != nil { 504 return err 505 } 506 // TODO(axw) 2016-08-04 #1609643 507 // Create global security group(s) here. 508 return nil 509 } 510 511 func (e *Environ) Bootstrap(ctx environs.BootstrapContext, args environs.BootstrapParams) (*environs.BootstrapResult, error) { 512 // The client's authentication may have been reset when finding tools if the agent-version 513 // attribute was updated so we need to re-authenticate. This will be a no-op if already authenticated. 514 // An authenticated client is needed for the URL() call below. 515 if err := authenticateClient(e.client); err != nil { 516 return nil, err 517 } 518 return common.Bootstrap(ctx, e, args) 519 } 520 521 // BootstrapMessage is part of the Environ interface. 522 func (e *Environ) BootstrapMessage() string { 523 return "" 524 } 525 526 func (e *Environ) ControllerInstances(controllerUUID string) ([]instance.Id, error) { 527 // Find all instances tagged with tags.JujuIsController. 528 instances, err := e.allControllerManagedInstances(controllerUUID, e.ecfg().useFloatingIP()) 529 if err != nil { 530 return nil, errors.Trace(err) 531 } 532 ids := make([]instance.Id, 0, 1) 533 for _, instance := range instances { 534 detail := instance.(*openstackInstance).getServerDetail() 535 if detail.Metadata[tags.JujuIsController] == "true" { 536 ids = append(ids, instance.Id()) 537 } 538 } 539 if len(ids) == 0 { 540 return nil, environs.ErrNoInstances 541 } 542 return ids, nil 543 } 544 545 func (e *Environ) Config() *config.Config { 546 return e.ecfg().Config 547 } 548 549 func newCredentials(spec environs.CloudSpec) (identity.Credentials, identity.AuthMode) { 550 credAttrs := spec.Credential.Attributes() 551 cred := identity.Credentials{ 552 Region: spec.Region, 553 URL: spec.Endpoint, 554 TenantName: credAttrs[CredAttrTenantName], 555 } 556 557 // AuthType is validated when the environment is opened, so it's known 558 // to be one of these values. 559 var authMode identity.AuthMode 560 switch spec.Credential.AuthType() { 561 case cloud.UserPassAuthType: 562 // TODO(axw) we need a way of saying to use legacy auth. 563 cred.User = credAttrs[CredAttrUserName] 564 cred.Secrets = credAttrs[CredAttrPassword] 565 cred.DomainName = credAttrs[CredAttrDomainName] 566 authMode = identity.AuthUserPass 567 if cred.DomainName != "" { 568 authMode = identity.AuthUserPassV3 569 } 570 case cloud.AccessKeyAuthType: 571 cred.User = credAttrs[CredAttrAccessKey] 572 cred.Secrets = credAttrs[CredAttrSecretKey] 573 authMode = identity.AuthKeyPair 574 } 575 return cred, authMode 576 } 577 578 func determineBestClient( 579 options identity.AuthOptions, 580 client client.AuthenticatingClient, 581 cred identity.Credentials, 582 newClient func(*identity.Credentials, identity.AuthMode, *log.Logger) client.AuthenticatingClient, 583 ) client.AuthenticatingClient { 584 for _, option := range options { 585 if option.Mode != identity.AuthUserPassV3 { 586 continue 587 } 588 cred.URL = option.Endpoint 589 v3client := newClient(&cred, identity.AuthUserPassV3, nil) 590 // V3 being advertised is not necessaritly a guarantee that it will 591 // work. 592 err := v3client.Authenticate() 593 if err == nil { 594 return v3client 595 } 596 } 597 return client 598 } 599 600 func authClient(spec environs.CloudSpec, ecfg *environConfig) (client.AuthenticatingClient, error) { 601 602 identityClientVersion, err := identityClientVersion(spec.Endpoint) 603 if err != nil { 604 return nil, errors.Annotate(err, "cannot create a client") 605 } 606 cred, authMode := newCredentials(spec) 607 608 newClient := client.NewClient 609 if ecfg.SSLHostnameVerification() == false { 610 newClient = client.NewNonValidatingClient 611 } 612 client := newClient(&cred, authMode, nil) 613 614 // before returning, lets make sure that we want to have AuthMode 615 // AuthUserPass instead of its V3 counterpart. 616 if authMode == identity.AuthUserPass && (identityClientVersion == -1 || identityClientVersion == 3) { 617 options, err := client.IdentityAuthOptions() 618 if err != nil { 619 logger.Errorf("cannot determine available auth versions %v", err) 620 } else { 621 client = determineBestClient(options, client, cred, newClient) 622 } 623 } 624 625 // By default, the client requires "compute" and 626 // "object-store". Juju only requires "compute". 627 client.SetRequiredServiceTypes([]string{"compute"}) 628 return client, nil 629 } 630 631 type authenticator interface { 632 Authenticate() error 633 } 634 635 var authenticateClient = func(auth authenticator) error { 636 err := auth.Authenticate() 637 if err != nil { 638 // Log the error in case there are any useful hints, 639 // but provide a readable and helpful error message 640 // to the user. 641 logger.Debugf("authentication failed: %v", err) 642 return errors.New(`authentication failed. 643 644 Please ensure the credentials are correct. A common mistake is 645 to specify the wrong tenant. Use the OpenStack "project" name 646 for tenant-name in your model configuration.`) 647 } 648 return nil 649 } 650 651 func (e *Environ) SetConfig(cfg *config.Config) error { 652 ecfg, err := providerInstance.newConfig(cfg) 653 if err != nil { 654 return err 655 } 656 // At this point, the authentication method config value has been validated so we extract it's value here 657 // to avoid having to validate again each time when creating the OpenStack client. 658 e.ecfgMutex.Lock() 659 defer e.ecfgMutex.Unlock() 660 e.ecfgUnlocked = ecfg 661 662 client, err := authClient(e.cloud, ecfg) 663 if err != nil { 664 return errors.Annotate(err, "cannot set config") 665 } 666 e.client = client 667 e.novaUnlocked = nova.New(e.client) 668 return nil 669 } 670 671 func identityClientVersion(authURL string) (int, error) { 672 url, err := url.Parse(authURL) 673 if err != nil { 674 return -1, err 675 } else if url.Path == "" { 676 return -1, err 677 } 678 // The last part of the path should be the version #. 679 // Example: https://keystone.foo:443/v3/ 680 logger.Tracef("authURL: %s", authURL) 681 versionNumStr := url.Path[2:] 682 if versionNumStr[len(versionNumStr)-1] == '/' { 683 versionNumStr = versionNumStr[:len(versionNumStr)-1] 684 } 685 major, _, err := version.ParseMajorMinor(versionNumStr) 686 return major, err 687 } 688 689 // getKeystoneImageSource is an imagemetadata.ImageDataSourceFunc that 690 // returns a DataSource using the "product-streams" keystone URL. 691 func getKeystoneImageSource(env environs.Environ) (simplestreams.DataSource, error) { 692 e, ok := env.(*Environ) 693 if !ok { 694 return nil, errors.NotSupportedf("non-openstack model") 695 } 696 return e.getKeystoneDataSource(&e.keystoneImageDataSourceMutex, &e.keystoneImageDataSource, "product-streams") 697 } 698 699 // getKeystoneToolsSource is a tools.ToolsDataSourceFunc that 700 // returns a DataSource using the "juju-tools" keystone URL. 701 func getKeystoneToolsSource(env environs.Environ) (simplestreams.DataSource, error) { 702 e, ok := env.(*Environ) 703 if !ok { 704 return nil, errors.NotSupportedf("non-openstack model") 705 } 706 return e.getKeystoneDataSource(&e.keystoneToolsDataSourceMutex, &e.keystoneToolsDataSource, "juju-tools") 707 } 708 709 func (e *Environ) getKeystoneDataSource(mu *sync.Mutex, datasource *simplestreams.DataSource, keystoneName string) (simplestreams.DataSource, error) { 710 mu.Lock() 711 defer mu.Unlock() 712 if *datasource != nil { 713 return *datasource, nil 714 } 715 if !e.client.IsAuthenticated() { 716 if err := authenticateClient(e.client); err != nil { 717 return nil, err 718 } 719 } 720 721 url, err := makeServiceURL(e.client, keystoneName, nil) 722 if err != nil { 723 return nil, errors.NewNotSupported(err, fmt.Sprintf("cannot make service URL: %v", err)) 724 } 725 verify := utils.VerifySSLHostnames 726 if !e.Config().SSLHostnameVerification() { 727 verify = utils.NoVerifySSLHostnames 728 } 729 *datasource = simplestreams.NewURLDataSource("keystone catalog", url, verify, simplestreams.SPECIFIC_CLOUD_DATA, false) 730 return *datasource, nil 731 } 732 733 // resolveNetwork takes either a network id or label and returns a network id 734 func (e *Environ) resolveNetwork(networkName string) (string, error) { 735 if utils.IsValidUUIDString(networkName) { 736 // Network id supplied, assume valid as boot will fail if not 737 return networkName, nil 738 } 739 // Network label supplied, resolve to a network id 740 networks, err := e.nova().ListNetworks() 741 if err != nil { 742 return "", err 743 } 744 var networkIds = []string{} 745 for _, network := range networks { 746 if network.Label == networkName { 747 networkIds = append(networkIds, network.Id) 748 } 749 } 750 switch len(networkIds) { 751 case 1: 752 return networkIds[0], nil 753 case 0: 754 return "", errors.Errorf("No networks exist with label %q", networkName) 755 } 756 return "", errors.Errorf("Multiple networks with label %q: %v", networkName, networkIds) 757 } 758 759 // allocatePublicIP tries to find an available floating IP address, or 760 // allocates a new one, returning it, or an error 761 func (e *Environ) allocatePublicIP() (*nova.FloatingIP, error) { 762 fips, err := e.nova().ListFloatingIPs() 763 if err != nil { 764 return nil, err 765 } 766 var newfip *nova.FloatingIP 767 for _, fip := range fips { 768 newfip = &fip 769 if fip.InstanceId != nil && *fip.InstanceId != "" { 770 // unavailable, skip 771 newfip = nil 772 continue 773 } else { 774 logger.Debugf("found unassigned public ip: %v", newfip.IP) 775 // unassigned, we can use it 776 return newfip, nil 777 } 778 } 779 if newfip == nil { 780 // allocate a new IP and use it 781 newfip, err = e.nova().AllocateFloatingIP() 782 if err != nil { 783 return nil, err 784 } 785 logger.Debugf("allocated new public IP: %v", newfip.IP) 786 } 787 return newfip, nil 788 } 789 790 // assignPublicIP tries to assign the given floating IP address to the 791 // specified server, or returns an error. 792 func (e *Environ) assignPublicIP(fip *nova.FloatingIP, serverId string) (err error) { 793 if fip == nil { 794 return errors.Errorf("cannot assign a nil public IP to %q", serverId) 795 } 796 if fip.InstanceId != nil && *fip.InstanceId == serverId { 797 // IP already assigned, nothing to do 798 return nil 799 } 800 // At startup nw_info is not yet cached so this may fail 801 // temporarily while the server is being built 802 for a := common.LongAttempt.Start(); a.Next(); { 803 err = e.nova().AddServerFloatingIP(serverId, fip.IP) 804 if err == nil { 805 return nil 806 } 807 } 808 return err 809 } 810 811 // DistributeInstances implements the state.InstanceDistributor policy. 812 func (e *Environ) DistributeInstances(candidates, distributionGroup []instance.Id) ([]instance.Id, error) { 813 return common.DistributeInstances(e, candidates, distributionGroup) 814 } 815 816 var availabilityZoneAllocations = common.AvailabilityZoneAllocations 817 818 // MaintainInstance is specified in the InstanceBroker interface. 819 func (*Environ) MaintainInstance(args environs.StartInstanceParams) error { 820 return nil 821 } 822 823 // StartInstance is specified in the InstanceBroker interface. 824 func (e *Environ) StartInstance(args environs.StartInstanceParams) (*environs.StartInstanceResult, error) { 825 if args.ControllerUUID == "" { 826 return nil, errors.New("missing controller UUID") 827 } 828 var availabilityZones []string 829 if args.Placement != "" { 830 placement, err := e.parsePlacement(args.Placement) 831 if err != nil { 832 return nil, err 833 } 834 if !placement.availabilityZone.State.Available { 835 return nil, errors.Errorf("availability zone %q is unavailable", placement.availabilityZone.Name) 836 } 837 availabilityZones = append(availabilityZones, placement.availabilityZone.Name) 838 } 839 840 // If no availability zone is specified, then automatically spread across 841 // the known zones for optimal spread across the instance distribution 842 // group. 843 if len(availabilityZones) == 0 { 844 var group []instance.Id 845 var err error 846 if args.DistributionGroup != nil { 847 group, err = args.DistributionGroup() 848 if err != nil { 849 return nil, err 850 } 851 } 852 zoneInstances, err := availabilityZoneAllocations(e, group) 853 if errors.IsNotImplemented(err) { 854 // Availability zones are an extension, so we may get a 855 // not implemented error; ignore these. 856 } else if err != nil { 857 return nil, err 858 } else { 859 for _, zone := range zoneInstances { 860 availabilityZones = append(availabilityZones, zone.ZoneName) 861 } 862 } 863 if len(availabilityZones) == 0 { 864 // No explicitly selectable zones available, so use an unspecified zone. 865 availabilityZones = []string{""} 866 } 867 } 868 869 series := args.Tools.OneSeries() 870 arches := args.Tools.Arches() 871 spec, err := findInstanceSpec(e, &instances.InstanceConstraint{ 872 Region: e.cloud.Region, 873 Series: series, 874 Arches: arches, 875 Constraints: args.Constraints, 876 }, args.ImageMetadata) 877 if err != nil { 878 return nil, err 879 } 880 tools, err := args.Tools.Match(tools.Filter{Arch: spec.Image.Arch}) 881 if err != nil { 882 return nil, errors.Errorf("chosen architecture %v not present in %v", spec.Image.Arch, arches) 883 } 884 885 if err := args.InstanceConfig.SetTools(tools); err != nil { 886 return nil, errors.Trace(err) 887 } 888 889 if err := instancecfg.FinishInstanceConfig(args.InstanceConfig, e.Config()); err != nil { 890 return nil, err 891 } 892 cloudcfg, err := e.configurator.GetCloudConfig(args) 893 if err != nil { 894 return nil, errors.Trace(err) 895 } 896 userData, err := providerinit.ComposeUserData(args.InstanceConfig, cloudcfg, OpenstackRenderer{}) 897 if err != nil { 898 return nil, errors.Annotate(err, "cannot make user data") 899 } 900 logger.Debugf("openstack user data; %d bytes", len(userData)) 901 902 var networks = e.firewaller.InitialNetworks() 903 usingNetwork := e.ecfg().network() 904 if usingNetwork != "" { 905 networkId, err := e.resolveNetwork(usingNetwork) 906 if err != nil { 907 return nil, err 908 } 909 logger.Debugf("using network id %q", networkId) 910 networks = append(networks, nova.ServerNetworks{NetworkId: networkId}) 911 } 912 withPublicIP := e.ecfg().useFloatingIP() 913 var publicIP *nova.FloatingIP 914 if withPublicIP { 915 logger.Debugf("allocating public IP address for openstack node") 916 if fip, err := e.allocatePublicIP(); err != nil { 917 return nil, errors.Annotate(err, "cannot allocate a public IP as needed") 918 } else { 919 publicIP = fip 920 logger.Infof("allocated public IP %s", publicIP.IP) 921 } 922 } 923 924 var apiPort int 925 if args.InstanceConfig.Controller != nil { 926 apiPort = args.InstanceConfig.Controller.Config.APIPort() 927 } else { 928 // All ports are the same so pick the first. 929 apiPort = args.InstanceConfig.APIInfo.Ports()[0] 930 } 931 var groupNames = make([]nova.SecurityGroupName, 0) 932 groups, err := e.firewaller.SetUpGroups(args.ControllerUUID, args.InstanceConfig.MachineId, apiPort) 933 if err != nil { 934 return nil, errors.Annotate(err, "cannot set up groups") 935 } 936 937 for _, g := range groups { 938 groupNames = append(groupNames, nova.SecurityGroupName{g.Name}) 939 } 940 machineName := resourceName( 941 e.namespace, 942 e.name, 943 args.InstanceConfig.MachineId, 944 ) 945 946 tryStartNovaInstance := func( 947 attempts utils.AttemptStrategy, 948 client *nova.Client, 949 instanceOpts nova.RunServerOpts, 950 ) (server *nova.Entity, err error) { 951 for a := attempts.Start(); a.Next(); { 952 server, err = client.RunServer(instanceOpts) 953 if err == nil || gooseerrors.IsNotFound(err) == false { 954 break 955 } 956 } 957 return server, err 958 } 959 960 tryStartNovaInstanceAcrossAvailZones := func( 961 attempts utils.AttemptStrategy, 962 client *nova.Client, 963 instanceOpts nova.RunServerOpts, 964 availabilityZones []string, 965 ) (server *nova.Entity, err error) { 966 for _, zone := range availabilityZones { 967 instanceOpts.AvailabilityZone = zone 968 e.configurator.ModifyRunServerOptions(&instanceOpts) 969 server, err = tryStartNovaInstance(attempts, client, instanceOpts) 970 if err == nil || isNoValidHostsError(err) == false { 971 break 972 } 973 974 logger.Infof("no valid hosts available in zone %q, trying another availability zone", zone) 975 } 976 977 if err != nil { 978 err = errors.Annotate(err, "cannot run instance") 979 } 980 981 return server, err 982 } 983 984 var opts = nova.RunServerOpts{ 985 Name: machineName, 986 FlavorId: spec.InstanceType.Id, 987 ImageId: spec.Image.Id, 988 UserData: userData, 989 SecurityGroupNames: groupNames, 990 Networks: networks, 991 Metadata: args.InstanceConfig.Tags, 992 } 993 server, err := tryStartNovaInstanceAcrossAvailZones(shortAttempt, e.nova(), opts, availabilityZones) 994 if err != nil { 995 return nil, errors.Trace(err) 996 } 997 998 detail, err := e.nova().GetServer(server.Id) 999 if err != nil { 1000 return nil, errors.Annotate(err, "cannot get started instance") 1001 } 1002 1003 inst := &openstackInstance{ 1004 e: e, 1005 serverDetail: detail, 1006 arch: &spec.Image.Arch, 1007 instType: &spec.InstanceType, 1008 } 1009 logger.Infof("started instance %q", inst.Id()) 1010 if withPublicIP { 1011 if err := e.assignPublicIP(publicIP, string(inst.Id())); err != nil { 1012 if err := e.terminateInstances([]instance.Id{inst.Id()}); err != nil { 1013 // ignore the failure at this stage, just log it 1014 logger.Debugf("failed to terminate instance %q: %v", inst.Id(), err) 1015 } 1016 return nil, errors.Annotatef(err, "cannot assign public address %s to instance %q", publicIP.IP, inst.Id()) 1017 } 1018 inst.floatingIP = publicIP 1019 logger.Infof("assigned public IP %s to %q", publicIP.IP, inst.Id()) 1020 } 1021 return &environs.StartInstanceResult{ 1022 Instance: inst, 1023 Hardware: inst.hardwareCharacteristics(), 1024 }, nil 1025 } 1026 1027 func isNoValidHostsError(err error) bool { 1028 if gooseErr, ok := err.(gooseerrors.Error); ok { 1029 if cause := gooseErr.Cause(); cause != nil { 1030 return strings.Contains(cause.Error(), "No valid host was found") 1031 } 1032 } 1033 return false 1034 } 1035 1036 func (e *Environ) StopInstances(ids ...instance.Id) error { 1037 // If in instance firewall mode, gather the security group names. 1038 securityGroupNames, err := e.firewaller.GetSecurityGroups(ids...) 1039 if err == environs.ErrNoInstances { 1040 return nil 1041 } 1042 if err != nil { 1043 return err 1044 } 1045 logger.Debugf("terminating instances %v", ids) 1046 if err := e.terminateInstances(ids); err != nil { 1047 return err 1048 } 1049 if securityGroupNames != nil { 1050 return e.deleteSecurityGroups(securityGroupNames) 1051 } 1052 return nil 1053 } 1054 1055 func (e *Environ) isAliveServer(server nova.ServerDetail) bool { 1056 switch server.Status { 1057 case nova.StatusActive, nova.StatusBuild, nova.StatusBuildSpawning, nova.StatusShutoff, nova.StatusSuspended: 1058 return true 1059 } 1060 return false 1061 } 1062 1063 func (e *Environ) listServers(ids []instance.Id) ([]nova.ServerDetail, error) { 1064 wantedServers := make([]nova.ServerDetail, 0, len(ids)) 1065 if len(ids) == 1 { 1066 // Common case, single instance, may return NotFound 1067 var maybeServer *nova.ServerDetail 1068 maybeServer, err := e.nova().GetServer(string(ids[0])) 1069 if err != nil { 1070 return nil, err 1071 } 1072 // Only return server details if it is currently alive 1073 if maybeServer != nil && e.isAliveServer(*maybeServer) { 1074 wantedServers = append(wantedServers, *maybeServer) 1075 } 1076 return wantedServers, nil 1077 } 1078 // List all instances in the environment. 1079 instances, err := e.AllInstances() 1080 if err != nil { 1081 return nil, err 1082 } 1083 // Return only servers with the wanted ids that are currently alive 1084 for _, inst := range instances { 1085 inst := inst.(*openstackInstance) 1086 serverDetail := *inst.serverDetail 1087 if !e.isAliveServer(serverDetail) { 1088 continue 1089 } 1090 for _, id := range ids { 1091 if inst.Id() != id { 1092 continue 1093 } 1094 wantedServers = append(wantedServers, serverDetail) 1095 break 1096 } 1097 } 1098 return wantedServers, nil 1099 } 1100 1101 // updateFloatingIPAddresses updates the instances with any floating IP address 1102 // that have been assigned to those instances. 1103 func (e *Environ) updateFloatingIPAddresses(instances map[string]instance.Instance) error { 1104 fips, err := e.nova().ListFloatingIPs() 1105 if err != nil { 1106 return err 1107 } 1108 for _, fip := range fips { 1109 if fip.InstanceId != nil && *fip.InstanceId != "" { 1110 instId := *fip.InstanceId 1111 if inst, ok := instances[instId]; ok { 1112 instFip := fip 1113 inst.(*openstackInstance).floatingIP = &instFip 1114 } 1115 } 1116 } 1117 return nil 1118 } 1119 1120 func (e *Environ) Instances(ids []instance.Id) ([]instance.Instance, error) { 1121 if len(ids) == 0 { 1122 return nil, nil 1123 } 1124 // Make a series of requests to cope with eventual consistency. 1125 // Each request will attempt to add more instances to the requested 1126 // set. 1127 var foundServers []nova.ServerDetail 1128 for a := shortAttempt.Start(); a.Next(); { 1129 var err error 1130 foundServers, err = e.listServers(ids) 1131 if err != nil { 1132 logger.Debugf("error listing servers: %v", err) 1133 if !gooseerrors.IsNotFound(err) { 1134 return nil, err 1135 } 1136 } 1137 if len(foundServers) == len(ids) { 1138 break 1139 } 1140 } 1141 logger.Tracef("%d/%d live servers found", len(foundServers), len(ids)) 1142 if len(foundServers) == 0 { 1143 return nil, environs.ErrNoInstances 1144 } 1145 1146 instsById := make(map[string]instance.Instance, len(foundServers)) 1147 for i, server := range foundServers { 1148 // TODO(wallyworld): lookup the flavor details to fill in the 1149 // instance type data 1150 instsById[server.Id] = &openstackInstance{ 1151 e: e, 1152 serverDetail: &foundServers[i], 1153 } 1154 } 1155 1156 // Update the instance structs with any floating IP address that has been assigned to the instance. 1157 if e.ecfg().useFloatingIP() { 1158 if err := e.updateFloatingIPAddresses(instsById); err != nil { 1159 return nil, err 1160 } 1161 } 1162 1163 insts := make([]instance.Instance, len(ids)) 1164 var err error 1165 for i, id := range ids { 1166 if inst := instsById[string(id)]; inst != nil { 1167 insts[i] = inst 1168 } else { 1169 err = environs.ErrPartialInstances 1170 } 1171 } 1172 return insts, err 1173 } 1174 1175 // AllInstances returns all instances in this environment. 1176 func (e *Environ) AllInstances() ([]instance.Instance, error) { 1177 tagFilter := tagValue{tags.JujuModel, e.ecfg().UUID()} 1178 return e.allInstances(tagFilter, e.ecfg().useFloatingIP()) 1179 } 1180 1181 // allControllerManagedInstances returns all instances managed by this 1182 // environment's controller, matching the optionally specified filter. 1183 func (e *Environ) allControllerManagedInstances(controllerUUID string, updateFloatingIPAddresses bool) ([]instance.Instance, error) { 1184 tagFilter := tagValue{tags.JujuController, controllerUUID} 1185 return e.allInstances(tagFilter, updateFloatingIPAddresses) 1186 } 1187 1188 type tagValue struct { 1189 tag, value string 1190 } 1191 1192 // allControllerManagedInstances returns all instances managed by this 1193 // environment's controller, matching the optionally specified filter. 1194 func (e *Environ) allInstances(tagFilter tagValue, updateFloatingIPAddresses bool) ([]instance.Instance, error) { 1195 servers, err := e.nova().ListServersDetail(jujuMachineFilter()) 1196 if err != nil { 1197 return nil, err 1198 } 1199 instsById := make(map[string]instance.Instance) 1200 for _, server := range servers { 1201 if server.Metadata[tagFilter.tag] != tagFilter.value { 1202 continue 1203 } 1204 if e.isAliveServer(server) { 1205 var s = server 1206 // TODO(wallyworld): lookup the flavor details to fill in the instance type data 1207 instsById[s.Id] = &openstackInstance{e: e, serverDetail: &s} 1208 } 1209 } 1210 if updateFloatingIPAddresses { 1211 if err := e.updateFloatingIPAddresses(instsById); err != nil { 1212 return nil, err 1213 } 1214 } 1215 insts := make([]instance.Instance, 0, len(instsById)) 1216 for _, inst := range instsById { 1217 insts = append(insts, inst) 1218 } 1219 return insts, nil 1220 } 1221 1222 func (e *Environ) Destroy() error { 1223 err := common.Destroy(e) 1224 if err != nil { 1225 return errors.Trace(err) 1226 } 1227 // Delete all security groups remaining in the model. 1228 return e.firewaller.DeleteAllModelGroups() 1229 } 1230 1231 // DestroyController implements the Environ interface. 1232 func (e *Environ) DestroyController(controllerUUID string) error { 1233 if err := e.Destroy(); err != nil { 1234 return errors.Annotate(err, "destroying controller model") 1235 } 1236 // In case any hosted environment hasn't been cleaned up yet, 1237 // we also attempt to delete their resources when the controller 1238 // environment is destroyed. 1239 if err := e.destroyControllerManagedEnvirons(controllerUUID); err != nil { 1240 return errors.Annotate(err, "destroying managed models") 1241 } 1242 return e.firewaller.DeleteAllControllerGroups(controllerUUID) 1243 } 1244 1245 // destroyControllerManagedEnvirons destroys all environments managed by this 1246 // models's controller. 1247 func (e *Environ) destroyControllerManagedEnvirons(controllerUUID string) error { 1248 // Terminate all instances managed by the controller. 1249 insts, err := e.allControllerManagedInstances(controllerUUID, false) 1250 if err != nil { 1251 return errors.Annotate(err, "listing instances") 1252 } 1253 instIds := make([]instance.Id, len(insts)) 1254 for i, inst := range insts { 1255 instIds[i] = inst.Id() 1256 } 1257 if err := e.terminateInstances(instIds); err != nil { 1258 return errors.Annotate(err, "terminating instances") 1259 } 1260 1261 // Delete all volumes managed by the controller. 1262 cinder, err := e.cinderProvider() 1263 if err == nil { 1264 volIds, err := allControllerManagedVolumes(cinder.storageAdapter, controllerUUID) 1265 if err != nil { 1266 return errors.Annotate(err, "listing volumes") 1267 } 1268 errs := destroyVolumes(cinder.storageAdapter, volIds) 1269 for i, err := range errs { 1270 if err == nil { 1271 continue 1272 } 1273 return errors.Annotatef(err, "destroying volume %q", volIds[i], err) 1274 } 1275 } else if !errors.IsNotSupported(err) { 1276 return errors.Trace(err) 1277 } 1278 1279 // Security groups for hosted models are destroyed by the 1280 // DeleteAllControllerGroups method call from Destroy(). 1281 return nil 1282 } 1283 1284 func allControllerManagedVolumes(storageAdapter OpenstackStorage, controllerUUID string) ([]string, error) { 1285 volumes, err := listVolumes(storageAdapter, func(v *cinder.Volume) bool { 1286 return v.Metadata[tags.JujuController] == controllerUUID 1287 }) 1288 if err != nil { 1289 return nil, errors.Trace(err) 1290 } 1291 volIds := make([]string, len(volumes)) 1292 for i, v := range volumes { 1293 volIds[i] = v.VolumeId 1294 } 1295 return volIds, nil 1296 } 1297 1298 func resourceName(namespace instance.Namespace, envName, resourceId string) string { 1299 return namespace.Value(envName + "-" + resourceId) 1300 } 1301 1302 // jujuMachineFilter returns a nova.Filter matching machines created by Juju. 1303 // The machines are not filtered to any particular environment. To do that, 1304 // instance tags must be compared. 1305 func jujuMachineFilter() *nova.Filter { 1306 filter := nova.NewFilter() 1307 filter.Set(nova.FilterServer, "juju-.*") 1308 return filter 1309 } 1310 1311 // portsToRuleInfo maps port ranges to nova rules 1312 func portsToRuleInfo(groupId string, ports []network.PortRange) []nova.RuleInfo { 1313 rules := make([]nova.RuleInfo, len(ports)) 1314 for i, portRange := range ports { 1315 rules[i] = nova.RuleInfo{ 1316 ParentGroupId: groupId, 1317 FromPort: portRange.FromPort, 1318 ToPort: portRange.ToPort, 1319 IPProtocol: portRange.Protocol, 1320 Cidr: "0.0.0.0/0", 1321 } 1322 } 1323 return rules 1324 } 1325 1326 func (e *Environ) OpenPorts(ports []network.PortRange) error { 1327 return e.firewaller.OpenPorts(ports) 1328 } 1329 1330 func (e *Environ) ClosePorts(ports []network.PortRange) error { 1331 return e.firewaller.ClosePorts(ports) 1332 } 1333 1334 func (e *Environ) Ports() ([]network.PortRange, error) { 1335 return e.firewaller.Ports() 1336 } 1337 1338 func (e *Environ) Provider() environs.EnvironProvider { 1339 return providerInstance 1340 } 1341 1342 // deleteSecurityGroups deletes the given security groups. If a security 1343 // group is also used by another environment (see bug #1300755), an attempt 1344 // to delete this group fails. A warning is logged in this case. 1345 func (e *Environ) deleteSecurityGroups(securityGroupNames []string) error { 1346 novaclient := e.nova() 1347 allSecurityGroups, err := novaclient.ListSecurityGroups() 1348 if err != nil { 1349 return err 1350 } 1351 for _, securityGroup := range allSecurityGroups { 1352 for _, name := range securityGroupNames { 1353 if securityGroup.Name == name { 1354 deleteSecurityGroup(novaclient, name, securityGroup.Id) 1355 break 1356 } 1357 } 1358 } 1359 return nil 1360 } 1361 1362 func (e *Environ) terminateInstances(ids []instance.Id) error { 1363 if len(ids) == 0 { 1364 return nil 1365 } 1366 var firstErr error 1367 novaClient := e.nova() 1368 for _, id := range ids { 1369 err := novaClient.DeleteServer(string(id)) 1370 if gooseerrors.IsNotFound(err) { 1371 err = nil 1372 } 1373 if err != nil && firstErr == nil { 1374 logger.Debugf("error terminating instance %q: %v", id, err) 1375 firstErr = err 1376 } 1377 } 1378 return firstErr 1379 } 1380 1381 // MetadataLookupParams returns parameters which are used to query simplestreams metadata. 1382 func (e *Environ) MetadataLookupParams(region string) (*simplestreams.MetadataLookupParams, error) { 1383 if region == "" { 1384 region = e.cloud.Region 1385 } 1386 cloudSpec, err := e.cloudSpec(region) 1387 if err != nil { 1388 return nil, err 1389 } 1390 return &simplestreams.MetadataLookupParams{ 1391 Series: config.PreferredSeries(e.ecfg()), 1392 Region: cloudSpec.Region, 1393 Endpoint: cloudSpec.Endpoint, 1394 }, nil 1395 } 1396 1397 // Region is specified in the HasRegion interface. 1398 func (e *Environ) Region() (simplestreams.CloudSpec, error) { 1399 return e.cloudSpec(e.cloud.Region) 1400 } 1401 1402 func (e *Environ) cloudSpec(region string) (simplestreams.CloudSpec, error) { 1403 return simplestreams.CloudSpec{ 1404 Region: region, 1405 Endpoint: e.cloud.Endpoint, 1406 }, nil 1407 } 1408 1409 // TagInstance implements environs.InstanceTagger. 1410 func (e *Environ) TagInstance(id instance.Id, tags map[string]string) error { 1411 if err := e.nova().SetServerMetadata(string(id), tags); err != nil { 1412 return errors.Annotate(err, "setting server metadata") 1413 } 1414 return nil 1415 } 1416 1417 func validateCloudSpec(spec environs.CloudSpec) error { 1418 if err := spec.Validate(); err != nil { 1419 return errors.Trace(err) 1420 } 1421 if err := validateAuthURL(spec.Endpoint); err != nil { 1422 return errors.Annotate(err, "validating auth-url") 1423 } 1424 if spec.Credential == nil { 1425 return errors.NotValidf("missing credential") 1426 } 1427 switch authType := spec.Credential.AuthType(); authType { 1428 case cloud.UserPassAuthType: 1429 case cloud.AccessKeyAuthType: 1430 default: 1431 return errors.NotSupportedf("%q auth-type", authType) 1432 } 1433 return nil 1434 } 1435 1436 func validateAuthURL(authURL string) error { 1437 parts, err := url.Parse(authURL) 1438 if err != nil || parts.Host == "" || parts.Scheme == "" { 1439 return errors.NotValidf("auth-url %q", authURL) 1440 } 1441 return nil 1442 }