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