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