launchpad.net/~rogpeppe/juju-core/500-errgo-fix@v0.0.0-20140213181702-000000002356/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  	"errors"
    10  	"fmt"
    11  	"io/ioutil"
    12  	"net/http"
    13  	"regexp"
    14  	"strings"
    15  	"sync"
    16  	"time"
    17  
    18  	"github.com/loggo/loggo"
    19  	"launchpad.net/goose/client"
    20  	gooseerrors "launchpad.net/goose/errors"
    21  	"launchpad.net/goose/identity"
    22  	"launchpad.net/goose/nova"
    23  	"launchpad.net/goose/swift"
    24  
    25  	"launchpad.net/juju-core/constraints"
    26  	"launchpad.net/juju-core/environs"
    27  	"launchpad.net/juju-core/environs/cloudinit"
    28  	"launchpad.net/juju-core/environs/config"
    29  	"launchpad.net/juju-core/environs/imagemetadata"
    30  	"launchpad.net/juju-core/environs/instances"
    31  	"launchpad.net/juju-core/environs/simplestreams"
    32  	"launchpad.net/juju-core/environs/storage"
    33  	envtools "launchpad.net/juju-core/environs/tools"
    34  	"launchpad.net/juju-core/instance"
    35  	"launchpad.net/juju-core/names"
    36  	"launchpad.net/juju-core/provider/common"
    37  	"launchpad.net/juju-core/state"
    38  	"launchpad.net/juju-core/state/api"
    39  	"launchpad.net/juju-core/tools"
    40  	"launchpad.net/juju-core/utils"
    41  )
    42  
    43  var logger = loggo.GetLogger("juju.provider.openstack")
    44  
    45  type environProvider struct{}
    46  
    47  var _ environs.EnvironProvider = (*environProvider)(nil)
    48  
    49  var providerInstance environProvider
    50  
    51  // Use shortAttempt to poll for short-term events.
    52  // TODO: This was kept to a long timeout because Nova needs more time than EC2.
    53  // For example, HP Cloud takes around 9.1 seconds (10 samples) to return a
    54  // BUILD(spawning) status. But storage delays are handled separately now, and
    55  // perhaps other polling attempts can time out faster.
    56  var shortAttempt = utils.AttemptStrategy{
    57  	Total: 15 * time.Second,
    58  	Delay: 200 * time.Millisecond,
    59  }
    60  
    61  func init() {
    62  	environs.RegisterProvider("openstack", environProvider{})
    63  }
    64  
    65  func (p environProvider) BoilerplateConfig() string {
    66  	return `
    67  # https://juju.ubuntu.com/docs/config-openstack.html
    68  openstack:
    69      type: openstack
    70      # use-floating-ip specifies whether a floating IP address is required
    71      # to give the nodes a public IP address. Some installations assign public IP
    72      # addresses by default without requiring a floating IP address.
    73      # use-floating-ip: false
    74  
    75      # use-default-secgroup specifies whether new machine instances should have the "default"
    76      # Openstack security group assigned.
    77      # use-default-secgroup: false
    78  
    79      # network specifies the network label or uuid to bring machines up on, in
    80      # the case where multiple networks exist. It may be omitted otherwise.
    81      # network: <your network label or uuid>
    82  
    83      # tools-metadata-url specifies the location of the Juju tools and metadata. It defaults to the
    84      # global public tools metadata location https://streams.canonical.com/tools.
    85      # tools-metadata-url:  https://you-tools-metadata-url
    86  
    87      # image-metadata-url specifies the location of Ubuntu cloud image metadata. It defaults to the
    88      # global public image metadata location https://cloud-images.ubuntu.com/releases.
    89      # image-metadata-url:  https://you-tools-metadata-url
    90  
    91      # image-stream chooses a simplestreams stream to select OS images from,
    92      # for example daily or released images (or any other stream available on simplestreams).
    93      # image-stream: "released"
    94  
    95      # auth-url defaults to the value of the environment variable OS_AUTH_URL,
    96      # but can be specified here.
    97      # auth-url: https://yourkeystoneurl:443/v2.0/
    98  
    99      # tenant-name holds the openstack tenant name. It defaults to
   100      # the environment variable OS_TENANT_NAME.
   101      # tenant-name: <your tenant name>
   102  
   103      # region holds the openstack region.  It defaults to
   104      # the environment variable OS_REGION_NAME.
   105      # region: <your region>
   106  
   107      # The auth-mode, username and password attributes
   108      # are used for userpass authentication (the default).
   109  
   110      # auth-mode holds the authentication mode. For user-password
   111      # authentication, auth-mode should be "userpass" and username
   112      # and password should be set appropriately; they default to
   113      # the environment variables OS_USERNAME and OS_PASSWORD
   114       # respectively.
   115      # auth-mode: userpass
   116      # username: <your username>
   117      # password: <secret>
   118       
   119      # For key-pair authentication, auth-mode should  be "keypair"
   120      # and access-key and secret-key should be  set appropriately; they default to
   121      # the environment variables OS_ACCESS_KEY and OS_SECRET_KEY
   122      # respectively.
   123      # auth-mode: keypair
   124      # access-key: <secret>
   125      # secret-key: <secret>
   126  
   127  # https://juju.ubuntu.com/docs/config-hpcloud.html
   128  hpcloud:
   129      type: openstack
   130      
   131      # use-floating-ip specifies whether a floating IP address is required
   132      # to give the nodes a public IP address. Some installations assign public IP
   133      # addresses by default without requiring a floating IP address.
   134      # use-floating-ip: false
   135  
   136      # use-default-secgroup specifies whether new machine instances should have the "default"
   137      # Openstack security group assigned.
   138      # use-default-secgroup: false
   139  
   140      # tenant-name holds the openstack tenant name. In HPCloud, this is 
   141      # synonymous with the project-name  It defaults to
   142      # the environment variable OS_TENANT_NAME.
   143      # tenant-name: <your tenant name>
   144      
   145      # auth-url holds the keystone url for authentication. 
   146      # It defaults to the value of the environment variable OS_AUTH_URL.
   147      # auth-url: https://region-a.geo-1.identity.hpcloudsvc.com:35357/v2.0/
   148  
   149      # region holds the HP Cloud region (e.g. az-1.region-a.geo-1).  
   150      # It defaults to the environment variable OS_REGION_NAME.
   151      # region: <your region>
   152      
   153      # auth-mode holds the authentication mode. For user-password
   154      # authentication, auth-mode should be "userpass" and username
   155      # and password should be set appropriately; they default to
   156      # the environment variables OS_USERNAME and OS_PASSWORD
   157      # respectively.
   158      # auth-mode: userpass
   159      # username: <your_username>
   160      # password: <your_password>
   161      
   162      # For key-pair authentication, auth-mode should  be "keypair"
   163      # and access-key and secret-key should be  set appropriately; they default to
   164      # the environment variables OS_ACCESS_KEY and OS_SECRET_KEY
   165      # respectively.
   166      # auth-mode: keypair
   167      # access-key: <secret>
   168      # secret-key: <secret>
   169  `[1:]
   170  }
   171  
   172  func (p environProvider) Open(cfg *config.Config) (environs.Environ, error) {
   173  	logger.Infof("opening environment %q", cfg.Name())
   174  	e := new(environ)
   175  	err := e.SetConfig(cfg)
   176  	if err != nil {
   177  		return nil, err
   178  	}
   179  	e.name = cfg.Name()
   180  	return e, nil
   181  }
   182  
   183  func (p environProvider) Prepare(cfg *config.Config) (environs.Environ, error) {
   184  	attrs := cfg.UnknownAttrs()
   185  	if _, ok := attrs["control-bucket"]; !ok {
   186  		uuid, err := utils.NewUUID()
   187  		if err != nil {
   188  			return nil, err
   189  		}
   190  		attrs["control-bucket"] = fmt.Sprintf("%x", uuid.Raw())
   191  	}
   192  	cfg, err := cfg.Apply(attrs)
   193  	if err != nil {
   194  		return nil, err
   195  	}
   196  	return p.Open(cfg)
   197  }
   198  
   199  // MetadataLookupParams returns parameters which are used to query image metadata to
   200  // find matching image information.
   201  func (p environProvider) MetadataLookupParams(region string) (*simplestreams.MetadataLookupParams, error) {
   202  	if region == "" {
   203  		return nil, fmt.Errorf("region must be specified")
   204  	}
   205  	return &simplestreams.MetadataLookupParams{
   206  		Region:        region,
   207  		Architectures: []string{"amd64", "arm"},
   208  	}, nil
   209  }
   210  
   211  func (p environProvider) SecretAttrs(cfg *config.Config) (map[string]string, error) {
   212  	m := make(map[string]string)
   213  	ecfg, err := providerInstance.newConfig(cfg)
   214  	if err != nil {
   215  		return nil, err
   216  	}
   217  	m["username"] = ecfg.username()
   218  	m["password"] = ecfg.password()
   219  	m["tenant-name"] = ecfg.tenantName()
   220  	return m, nil
   221  }
   222  
   223  func (p environProvider) PublicAddress() (string, error) {
   224  	if addr, err := fetchMetadata("public-ipv4"); err != nil {
   225  		return "", err
   226  	} else if addr != "" {
   227  		return addr, nil
   228  	}
   229  	return p.PrivateAddress()
   230  }
   231  
   232  func (p environProvider) PrivateAddress() (string, error) {
   233  	return fetchMetadata("local-ipv4")
   234  }
   235  
   236  // metadataHost holds the address of the instance metadata service.
   237  // It is a variable so that tests can change it to refer to a local
   238  // server when needed.
   239  var metadataHost = "http://169.254.169.254"
   240  
   241  // fetchMetadata fetches a single atom of data from the openstack instance metadata service.
   242  // http://docs.amazonwebservices.com/AWSEC2/latest/UserGuide/AESDG-chapter-instancedata.html
   243  // (the same specs is implemented in ec2, hence the reference)
   244  func fetchMetadata(name string) (value string, err error) {
   245  	uri := fmt.Sprintf("%s/latest/meta-data/%s", metadataHost, name)
   246  	data, err := retryGet(uri)
   247  	if err != nil {
   248  		return "", err
   249  	}
   250  	return strings.TrimSpace(string(data)), nil
   251  }
   252  
   253  func retryGet(uri string) (data []byte, err error) {
   254  	for a := shortAttempt.Start(); a.Next(); {
   255  		var resp *http.Response
   256  		resp, err = http.Get(uri)
   257  		if err != nil {
   258  			continue
   259  		}
   260  		defer resp.Body.Close()
   261  		if resp.StatusCode != http.StatusOK {
   262  			err = fmt.Errorf("bad http response %v", resp.Status)
   263  			continue
   264  		}
   265  		var data []byte
   266  		data, err = ioutil.ReadAll(resp.Body)
   267  		if err != nil {
   268  			continue
   269  		}
   270  		return data, nil
   271  	}
   272  	if err != nil {
   273  		return nil, fmt.Errorf("cannot get %q: %v", uri, err)
   274  	}
   275  	return
   276  }
   277  
   278  type environ struct {
   279  	name string
   280  
   281  	ecfgMutex       sync.Mutex
   282  	imageBaseMutex  sync.Mutex
   283  	toolsBaseMutex  sync.Mutex
   284  	ecfgUnlocked    *environConfig
   285  	client          client.AuthenticatingClient
   286  	novaUnlocked    *nova.Client
   287  	storageUnlocked storage.Storage
   288  	// An ordered list of sources in which to find the simplestreams index files used to
   289  	// look up image ids.
   290  	imageSources []simplestreams.DataSource
   291  	// An ordered list of paths in which to find the simplestreams index files used to
   292  	// look up tools ids.
   293  	toolsSources []simplestreams.DataSource
   294  }
   295  
   296  var _ environs.Environ = (*environ)(nil)
   297  var _ imagemetadata.SupportsCustomSources = (*environ)(nil)
   298  var _ envtools.SupportsCustomSources = (*environ)(nil)
   299  var _ simplestreams.HasRegion = (*environ)(nil)
   300  
   301  type openstackInstance struct {
   302  	e        *environ
   303  	instType *instances.InstanceType
   304  	arch     *string
   305  
   306  	mu           sync.Mutex
   307  	serverDetail *nova.ServerDetail
   308  }
   309  
   310  func (inst *openstackInstance) String() string {
   311  	return string(inst.Id())
   312  }
   313  
   314  var _ instance.Instance = (*openstackInstance)(nil)
   315  
   316  func (inst *openstackInstance) Refresh() error {
   317  	inst.mu.Lock()
   318  	defer inst.mu.Unlock()
   319  	server, err := inst.e.nova().GetServer(inst.serverDetail.Id)
   320  	if err != nil {
   321  		return err
   322  	}
   323  	inst.serverDetail = server
   324  	return nil
   325  }
   326  
   327  func (inst *openstackInstance) getServerDetail() *nova.ServerDetail {
   328  	inst.mu.Lock()
   329  	defer inst.mu.Unlock()
   330  	return inst.serverDetail
   331  }
   332  
   333  func (inst *openstackInstance) Id() instance.Id {
   334  	return instance.Id(inst.getServerDetail().Id)
   335  }
   336  
   337  func (inst *openstackInstance) Status() string {
   338  	return inst.getServerDetail().Status
   339  }
   340  
   341  func (inst *openstackInstance) hardwareCharacteristics() *instance.HardwareCharacteristics {
   342  	hc := &instance.HardwareCharacteristics{Arch: inst.arch}
   343  	if inst.instType != nil {
   344  		hc.Mem = &inst.instType.Mem
   345  		// openstack is special in that a 0-size root disk means that
   346  		// the root disk will result in an instance with a root disk
   347  		// the same size as the image that created it, so we just set
   348  		// the HardwareCharacteristics to nil to signal that we don't
   349  		// know what the correct size is.
   350  		if inst.instType.RootDisk == 0 {
   351  			hc.RootDisk = nil
   352  		} else {
   353  			hc.RootDisk = &inst.instType.RootDisk
   354  		}
   355  		hc.CpuCores = &inst.instType.CpuCores
   356  		hc.CpuPower = inst.instType.CpuPower
   357  		// tags not currently supported on openstack
   358  	}
   359  	return hc
   360  }
   361  
   362  // getAddress returns the existing server information on addresses,
   363  // but fetches the details over the api again if no addresses exist.
   364  func (inst *openstackInstance) getAddresses() (map[string][]nova.IPAddress, error) {
   365  	addrs := inst.getServerDetail().Addresses
   366  	if len(addrs) == 0 {
   367  		server, err := inst.e.nova().GetServer(string(inst.Id()))
   368  		if err != nil {
   369  			return nil, err
   370  		}
   371  		addrs = server.Addresses
   372  	}
   373  	return addrs, nil
   374  }
   375  
   376  // Addresses implements instance.Addresses() returning generic address
   377  // details for the instances, and calling the openstack api if needed.
   378  func (inst *openstackInstance) Addresses() ([]instance.Address, error) {
   379  	addresses, err := inst.getAddresses()
   380  	if err != nil {
   381  		return nil, err
   382  	}
   383  	return convertNovaAddresses(addresses), nil
   384  }
   385  
   386  // convertNovaAddresses returns nova addresses in generic format
   387  func convertNovaAddresses(addresses map[string][]nova.IPAddress) []instance.Address {
   388  	// TODO(gz) Network ordering may be significant but is not preserved by
   389  	// the map, see lp:1188126 for example. That could potentially be fixed
   390  	// in goose, or left to be derived by other means.
   391  	var machineAddresses []instance.Address
   392  	for network, ips := range addresses {
   393  		networkscope := instance.NetworkUnknown
   394  		// For canonistack and hpcloud, public floating addresses may
   395  		// be put in networks named something other than public. Rely
   396  		// on address sanity logic to catch and mark them corectly.
   397  		if network == "public" {
   398  			networkscope = instance.NetworkPublic
   399  		}
   400  		for _, address := range ips {
   401  			// Assume ipv4 unless specified otherwise
   402  			addrtype := instance.Ipv4Address
   403  			if address.Version == 6 {
   404  				addrtype = instance.Ipv6Address
   405  			}
   406  			// TODO(gz): Use NewAddress... with sanity checking
   407  			machineAddr := instance.Address{
   408  				Value:        address.Address,
   409  				Type:         addrtype,
   410  				NetworkName:  network,
   411  				NetworkScope: networkscope,
   412  			}
   413  			machineAddresses = append(machineAddresses, machineAddr)
   414  		}
   415  	}
   416  	return machineAddresses
   417  }
   418  
   419  func (inst *openstackInstance) DNSName() (string, error) {
   420  	addresses, err := inst.Addresses()
   421  	if err != nil {
   422  		return "", err
   423  	}
   424  	addr := instance.SelectPublicAddress(addresses)
   425  	if addr == "" {
   426  		return "", instance.ErrNoDNSName
   427  	}
   428  	return addr, nil
   429  }
   430  
   431  func (inst *openstackInstance) WaitDNSName() (string, error) {
   432  	return common.WaitDNSName(inst)
   433  }
   434  
   435  // TODO: following 30 lines nearly verbatim from environs/ec2
   436  
   437  func (inst *openstackInstance) OpenPorts(machineId string, ports []instance.Port) error {
   438  	if inst.e.Config().FirewallMode() != config.FwInstance {
   439  		return fmt.Errorf("invalid firewall mode %q for opening ports on instance",
   440  			inst.e.Config().FirewallMode())
   441  	}
   442  	name := inst.e.machineGroupName(machineId)
   443  	if err := inst.e.openPortsInGroup(name, ports); err != nil {
   444  		return err
   445  	}
   446  	logger.Infof("opened ports in security group %s: %v", name, ports)
   447  	return nil
   448  }
   449  
   450  func (inst *openstackInstance) ClosePorts(machineId string, ports []instance.Port) error {
   451  	if inst.e.Config().FirewallMode() != config.FwInstance {
   452  		return fmt.Errorf("invalid firewall mode %q for closing ports on instance",
   453  			inst.e.Config().FirewallMode())
   454  	}
   455  	name := inst.e.machineGroupName(machineId)
   456  	if err := inst.e.closePortsInGroup(name, ports); err != nil {
   457  		return err
   458  	}
   459  	logger.Infof("closed ports in security group %s: %v", name, ports)
   460  	return nil
   461  }
   462  
   463  func (inst *openstackInstance) Ports(machineId string) ([]instance.Port, error) {
   464  	if inst.e.Config().FirewallMode() != config.FwInstance {
   465  		return nil, fmt.Errorf("invalid firewall mode %q for retrieving ports from instance",
   466  			inst.e.Config().FirewallMode())
   467  	}
   468  	name := inst.e.machineGroupName(machineId)
   469  	return inst.e.portsInGroup(name)
   470  }
   471  
   472  func (e *environ) ecfg() *environConfig {
   473  	e.ecfgMutex.Lock()
   474  	ecfg := e.ecfgUnlocked
   475  	e.ecfgMutex.Unlock()
   476  	return ecfg
   477  }
   478  
   479  func (e *environ) nova() *nova.Client {
   480  	e.ecfgMutex.Lock()
   481  	nova := e.novaUnlocked
   482  	e.ecfgMutex.Unlock()
   483  	return nova
   484  }
   485  
   486  // PrecheckInstance is specified in the environs.Prechecker interface.
   487  func (*environ) PrecheckInstance(series string, cons constraints.Value) error {
   488  	return nil
   489  }
   490  
   491  // PrecheckContainer is specified in the environs.Prechecker interface.
   492  func (*environ) PrecheckContainer(series string, kind instance.ContainerType) error {
   493  	// This check can either go away or be relaxed when the openstack
   494  	// provider manages container addressibility.
   495  	return environs.NewContainersUnsupported("openstack provider does not support containers")
   496  }
   497  
   498  func (e *environ) Name() string {
   499  	return e.name
   500  }
   501  
   502  func (e *environ) Storage() storage.Storage {
   503  	e.ecfgMutex.Lock()
   504  	stor := e.storageUnlocked
   505  	e.ecfgMutex.Unlock()
   506  	return stor
   507  }
   508  
   509  func (e *environ) Bootstrap(ctx environs.BootstrapContext, cons constraints.Value) error {
   510  	// The client's authentication may have been reset when finding tools if the agent-version
   511  	// attribute was updated so we need to re-authenticate. This will be a no-op if already authenticated.
   512  	// An authenticated client is needed for the URL() call below.
   513  	err := e.client.Authenticate()
   514  	if err != nil {
   515  		return err
   516  	}
   517  	return common.Bootstrap(ctx, e, cons)
   518  }
   519  
   520  func (e *environ) StateInfo() (*state.Info, *api.Info, error) {
   521  	return common.StateInfo(e)
   522  }
   523  
   524  func (e *environ) Config() *config.Config {
   525  	return e.ecfg().Config
   526  }
   527  
   528  func (e *environ) authClient(ecfg *environConfig, authModeCfg AuthMode) client.AuthenticatingClient {
   529  	cred := &identity.Credentials{
   530  		User:       ecfg.username(),
   531  		Secrets:    ecfg.password(),
   532  		Region:     ecfg.region(),
   533  		TenantName: ecfg.tenantName(),
   534  		URL:        ecfg.authURL(),
   535  	}
   536  	// authModeCfg has already been validated so we know it's one of the values below.
   537  	var authMode identity.AuthMode
   538  	switch authModeCfg {
   539  	case AuthLegacy:
   540  		authMode = identity.AuthLegacy
   541  	case AuthUserPass:
   542  		authMode = identity.AuthUserPass
   543  	case AuthKeyPair:
   544  		authMode = identity.AuthKeyPair
   545  		cred.User = ecfg.accessKey()
   546  		cred.Secrets = ecfg.secretKey()
   547  	}
   548  	newClient := client.NewClient
   549  	if !ecfg.SSLHostnameVerification() {
   550  		newClient = client.NewNonValidatingClient
   551  	}
   552  	return newClient(cred, authMode, nil)
   553  }
   554  
   555  func (e *environ) SetConfig(cfg *config.Config) error {
   556  	ecfg, err := providerInstance.newConfig(cfg)
   557  	if err != nil {
   558  		return err
   559  	}
   560  	// At this point, the authentication method config value has been validated so we extract it's value here
   561  	// to avoid having to validate again each time when creating the OpenStack client.
   562  	var authModeCfg AuthMode
   563  	e.ecfgMutex.Lock()
   564  	defer e.ecfgMutex.Unlock()
   565  	authModeCfg = AuthMode(ecfg.authMode())
   566  	e.ecfgUnlocked = ecfg
   567  
   568  	e.client = e.authClient(ecfg, authModeCfg)
   569  	e.novaUnlocked = nova.New(e.client)
   570  
   571  	// create new control storage instance, existing instances continue
   572  	// to reference their existing configuration.
   573  	// public storage instance creation is deferred until needed since authenticated
   574  	// access to the identity service is required so that any juju-tools endpoint can be used.
   575  	e.storageUnlocked = &openstackstorage{
   576  		containerName: ecfg.controlBucket(),
   577  		// this is possibly just a hack - if the ACL is swift.Private,
   578  		// the machine won't be able to get the tools (401 error)
   579  		containerACL: swift.PublicRead,
   580  		swift:        swift.New(e.client)}
   581  	return nil
   582  }
   583  
   584  // GetImageSources returns a list of sources which are used to search for simplestreams image metadata.
   585  func (e *environ) GetImageSources() ([]simplestreams.DataSource, error) {
   586  	e.imageBaseMutex.Lock()
   587  	defer e.imageBaseMutex.Unlock()
   588  
   589  	if e.imageSources != nil {
   590  		return e.imageSources, nil
   591  	}
   592  	if !e.client.IsAuthenticated() {
   593  		err := e.client.Authenticate()
   594  		if err != nil {
   595  			return nil, err
   596  		}
   597  	}
   598  	// Add the simplestreams source off the control bucket.
   599  	e.imageSources = append(e.imageSources, storage.NewStorageSimpleStreamsDataSource(
   600  		e.Storage(), storage.BaseImagesPath))
   601  	// Add the simplestreams base URL from keystone if it is defined.
   602  	productStreamsURL, err := e.client.MakeServiceURL("product-streams", nil)
   603  	if err == nil {
   604  		verify := simplestreams.VerifySSLHostnames
   605  		if !e.Config().SSLHostnameVerification() {
   606  			verify = simplestreams.NoVerifySSLHostnames
   607  		}
   608  		source := simplestreams.NewURLDataSource(productStreamsURL, verify)
   609  		e.imageSources = append(e.imageSources, source)
   610  	}
   611  	return e.imageSources, nil
   612  }
   613  
   614  // GetToolsSources returns a list of sources which are used to search for simplestreams tools metadata.
   615  func (e *environ) GetToolsSources() ([]simplestreams.DataSource, error) {
   616  	e.toolsBaseMutex.Lock()
   617  	defer e.toolsBaseMutex.Unlock()
   618  
   619  	if e.toolsSources != nil {
   620  		return e.toolsSources, nil
   621  	}
   622  	if !e.client.IsAuthenticated() {
   623  		err := e.client.Authenticate()
   624  		if err != nil {
   625  			return nil, err
   626  		}
   627  	}
   628  	verify := simplestreams.VerifySSLHostnames
   629  	if !e.Config().SSLHostnameVerification() {
   630  		verify = simplestreams.NoVerifySSLHostnames
   631  	}
   632  	// Add the simplestreams source off the control bucket.
   633  	e.toolsSources = append(e.toolsSources, storage.NewStorageSimpleStreamsDataSource(e.Storage(), storage.BaseToolsPath))
   634  	// Add the simplestreams base URL from keystone if it is defined.
   635  	toolsURL, err := e.client.MakeServiceURL("juju-tools", nil)
   636  	if err == nil {
   637  		source := simplestreams.NewURLDataSource(toolsURL, verify)
   638  		e.toolsSources = append(e.toolsSources, source)
   639  	}
   640  	return e.toolsSources, nil
   641  }
   642  
   643  // TODO(gz): Move this somewhere more reusable
   644  const uuidPattern = "^([a-fA-F0-9]{8})-([a-fA-f0-9]{4})-([1-5][a-fA-f0-9]{3})-([a-fA-f0-9]{4})-([a-fA-f0-9]{12})$"
   645  
   646  var uuidRegexp = regexp.MustCompile(uuidPattern)
   647  
   648  // resolveNetwork takes either a network id or label and returns a network id
   649  func (e *environ) resolveNetwork(networkName string) (string, error) {
   650  	if uuidRegexp.MatchString(networkName) {
   651  		// Network id supplied, assume valid as boot will fail if not
   652  		return networkName, nil
   653  	}
   654  	// Network label supplied, resolve to a network id
   655  	networks, err := e.nova().ListNetworks()
   656  	if err != nil {
   657  		return "", err
   658  	}
   659  	var networkIds = []string{}
   660  	for _, network := range networks {
   661  		if network.Label == networkName {
   662  			networkIds = append(networkIds, network.Id)
   663  		}
   664  	}
   665  	switch len(networkIds) {
   666  	case 1:
   667  		return networkIds[0], nil
   668  	case 0:
   669  		return "", fmt.Errorf("No networks exist with label %q", networkName)
   670  	}
   671  	return "", fmt.Errorf("Multiple networks with label %q: %v", networkName, networkIds)
   672  }
   673  
   674  // allocatePublicIP tries to find an available floating IP address, or
   675  // allocates a new one, returning it, or an error
   676  func (e *environ) allocatePublicIP() (*nova.FloatingIP, error) {
   677  	fips, err := e.nova().ListFloatingIPs()
   678  	if err != nil {
   679  		return nil, err
   680  	}
   681  	var newfip *nova.FloatingIP
   682  	for _, fip := range fips {
   683  		newfip = &fip
   684  		if fip.InstanceId != nil && *fip.InstanceId != "" {
   685  			// unavailable, skip
   686  			newfip = nil
   687  			continue
   688  		} else {
   689  			// unassigned, we can use it
   690  			return newfip, nil
   691  		}
   692  	}
   693  	if newfip == nil {
   694  		// allocate a new IP and use it
   695  		newfip, err = e.nova().AllocateFloatingIP()
   696  		if err != nil {
   697  			return nil, err
   698  		}
   699  	}
   700  	return newfip, nil
   701  }
   702  
   703  // assignPublicIP tries to assign the given floating IP address to the
   704  // specified server, or returns an error.
   705  func (e *environ) assignPublicIP(fip *nova.FloatingIP, serverId string) (err error) {
   706  	if fip == nil {
   707  		return fmt.Errorf("cannot assign a nil public IP to %q", serverId)
   708  	}
   709  	if fip.InstanceId != nil && *fip.InstanceId == serverId {
   710  		// IP already assigned, nothing to do
   711  		return nil
   712  	}
   713  	// At startup nw_info is not yet cached so this may fail
   714  	// temporarily while the server is being built
   715  	for a := common.LongAttempt.Start(); a.Next(); {
   716  		err = e.nova().AddServerFloatingIP(serverId, fip.IP)
   717  		if err == nil {
   718  			return nil
   719  		}
   720  	}
   721  	return err
   722  }
   723  
   724  // StartInstance is specified in the InstanceBroker interface.
   725  func (e *environ) StartInstance(cons constraints.Value, possibleTools tools.List,
   726  	machineConfig *cloudinit.MachineConfig) (instance.Instance, *instance.HardwareCharacteristics, error) {
   727  
   728  	series := possibleTools.OneSeries()
   729  	arches := possibleTools.Arches()
   730  	spec, err := findInstanceSpec(e, &instances.InstanceConstraint{
   731  		Region:      e.ecfg().region(),
   732  		Series:      series,
   733  		Arches:      arches,
   734  		Constraints: cons,
   735  	})
   736  	if err != nil {
   737  		return nil, nil, err
   738  	}
   739  	tools, err := possibleTools.Match(tools.Filter{Arch: spec.Image.Arch})
   740  	if err != nil {
   741  		return nil, nil, fmt.Errorf("chosen architecture %v not present in %v", spec.Image.Arch, arches)
   742  	}
   743  
   744  	machineConfig.Tools = tools[0]
   745  
   746  	if err := environs.FinishMachineConfig(machineConfig, e.Config(), cons); err != nil {
   747  		return nil, nil, err
   748  	}
   749  	userData, err := environs.ComposeUserData(machineConfig)
   750  	if err != nil {
   751  		return nil, nil, fmt.Errorf("cannot make user data: %v", err)
   752  	}
   753  	logger.Debugf("openstack user data; %d bytes", len(userData))
   754  	var networks = []nova.ServerNetworks{}
   755  	usingNetwork := e.ecfg().network()
   756  	if usingNetwork != "" {
   757  		networkId, err := e.resolveNetwork(usingNetwork)
   758  		if err != nil {
   759  			return nil, nil, err
   760  		}
   761  		logger.Debugf("using network id %q", networkId)
   762  		networks = append(networks, nova.ServerNetworks{NetworkId: networkId})
   763  	}
   764  	withPublicIP := e.ecfg().useFloatingIP()
   765  	var publicIP *nova.FloatingIP
   766  	if withPublicIP {
   767  		if fip, err := e.allocatePublicIP(); err != nil {
   768  			return nil, nil, fmt.Errorf("cannot allocate a public IP as needed: %v", err)
   769  		} else {
   770  			publicIP = fip
   771  			logger.Infof("allocated public IP %s", publicIP.IP)
   772  		}
   773  	}
   774  	cfg := e.Config()
   775  	groups, err := e.setUpGroups(machineConfig.MachineId, cfg.StatePort(), cfg.APIPort())
   776  	if err != nil {
   777  		return nil, nil, fmt.Errorf("cannot set up groups: %v", err)
   778  	}
   779  	var groupNames = make([]nova.SecurityGroupName, len(groups))
   780  	for i, g := range groups {
   781  		groupNames[i] = nova.SecurityGroupName{g.Name}
   782  	}
   783  	var opts = nova.RunServerOpts{
   784  		Name:               e.machineFullName(machineConfig.MachineId),
   785  		FlavorId:           spec.InstanceType.Id,
   786  		ImageId:            spec.Image.Id,
   787  		UserData:           userData,
   788  		SecurityGroupNames: groupNames,
   789  		Networks:           networks,
   790  	}
   791  	var server *nova.Entity
   792  	for a := shortAttempt.Start(); a.Next(); {
   793  		server, err = e.nova().RunServer(opts)
   794  		if err == nil || !gooseerrors.IsNotFound(err) {
   795  			break
   796  		}
   797  	}
   798  	if err != nil {
   799  		return nil, nil, fmt.Errorf("cannot run instance: %v", err)
   800  	}
   801  	detail, err := e.nova().GetServer(server.Id)
   802  	if err != nil {
   803  		return nil, nil, fmt.Errorf("cannot get started instance: %v", err)
   804  	}
   805  	inst := &openstackInstance{
   806  		e:            e,
   807  		serverDetail: detail,
   808  		arch:         &spec.Image.Arch,
   809  		instType:     &spec.InstanceType,
   810  	}
   811  	logger.Infof("started instance %q", inst.Id())
   812  	if withPublicIP {
   813  		if err := e.assignPublicIP(publicIP, string(inst.Id())); err != nil {
   814  			if err := e.terminateInstances([]instance.Id{inst.Id()}); err != nil {
   815  				// ignore the failure at this stage, just log it
   816  				logger.Debugf("failed to terminate instance %q: %v", inst.Id(), err)
   817  			}
   818  			return nil, nil, fmt.Errorf("cannot assign public address %s to instance %q: %v", publicIP.IP, inst.Id(), err)
   819  		}
   820  		logger.Infof("assigned public IP %s to %q", publicIP.IP, inst.Id())
   821  	}
   822  	return inst, inst.hardwareCharacteristics(), nil
   823  }
   824  
   825  func (e *environ) StopInstances(insts []instance.Instance) error {
   826  	ids := make([]instance.Id, len(insts))
   827  	for i, inst := range insts {
   828  		instanceValue, ok := inst.(*openstackInstance)
   829  		if !ok {
   830  			return errors.New("Incompatible instance.Instance supplied")
   831  		}
   832  		ids[i] = instanceValue.Id()
   833  	}
   834  	logger.Debugf("terminating instances %v", ids)
   835  	return e.terminateInstances(ids)
   836  }
   837  
   838  // collectInstances tries to get information on each instance id in ids.
   839  // It fills the slots in the given map for known servers with status
   840  // either ACTIVE or BUILD. Returns a list of missing ids.
   841  func (e *environ) collectInstances(ids []instance.Id, out map[instance.Id]instance.Instance) []instance.Id {
   842  	var err error
   843  	serversById := make(map[string]nova.ServerDetail)
   844  	if len(ids) == 1 {
   845  		// most common case - single instance
   846  		var server *nova.ServerDetail
   847  		server, err = e.nova().GetServer(string(ids[0]))
   848  		if server != nil {
   849  			serversById[server.Id] = *server
   850  		}
   851  	} else {
   852  		var servers []nova.ServerDetail
   853  		servers, err = e.nova().ListServersDetail(e.machinesFilter())
   854  		for _, server := range servers {
   855  			serversById[server.Id] = server
   856  		}
   857  	}
   858  	if err != nil {
   859  		return ids
   860  	}
   861  	var missing []instance.Id
   862  	for _, id := range ids {
   863  		if server, found := serversById[string(id)]; found {
   864  			// HPCloud uses "BUILD(spawning)" as an intermediate BUILD states once networking is available.
   865  			switch server.Status {
   866  			case nova.StatusActive, nova.StatusBuild, nova.StatusBuildSpawning:
   867  				// TODO(wallyworld): lookup the flavor details to fill in the instance type data
   868  				out[id] = &openstackInstance{e: e, serverDetail: &server}
   869  				continue
   870  			}
   871  		}
   872  		missing = append(missing, id)
   873  	}
   874  	return missing
   875  }
   876  
   877  func (e *environ) Instances(ids []instance.Id) ([]instance.Instance, error) {
   878  	if len(ids) == 0 {
   879  		return nil, nil
   880  	}
   881  	missing := ids
   882  	found := make(map[instance.Id]instance.Instance)
   883  	// Make a series of requests to cope with eventual consistency.
   884  	// Each request will attempt to add more instances to the requested
   885  	// set.
   886  	for a := shortAttempt.Start(); a.Next(); {
   887  		if missing = e.collectInstances(missing, found); len(missing) == 0 {
   888  			break
   889  		}
   890  	}
   891  	if len(found) == 0 {
   892  		return nil, environs.ErrNoInstances
   893  	}
   894  	insts := make([]instance.Instance, len(ids))
   895  	var err error
   896  	for i, id := range ids {
   897  		if inst := found[id]; inst != nil {
   898  			insts[i] = inst
   899  		} else {
   900  			err = environs.ErrPartialInstances
   901  		}
   902  	}
   903  	return insts, err
   904  }
   905  
   906  func (e *environ) AllInstances() (insts []instance.Instance, err error) {
   907  	servers, err := e.nova().ListServersDetail(e.machinesFilter())
   908  	if err != nil {
   909  		return nil, err
   910  	}
   911  	for _, server := range servers {
   912  		if server.Status == nova.StatusActive || server.Status == nova.StatusBuild {
   913  			var s = server
   914  			// TODO(wallyworld): lookup the flavor details to fill in the instance type data
   915  			insts = append(insts, &openstackInstance{
   916  				e:            e,
   917  				serverDetail: &s,
   918  			})
   919  		}
   920  	}
   921  	return insts, err
   922  }
   923  
   924  func (e *environ) Destroy() error {
   925  	return common.Destroy(e)
   926  }
   927  
   928  func (e *environ) globalGroupName() string {
   929  	return fmt.Sprintf("%s-global", e.jujuGroupName())
   930  }
   931  
   932  func (e *environ) machineGroupName(machineId string) string {
   933  	return fmt.Sprintf("%s-%s", e.jujuGroupName(), machineId)
   934  }
   935  
   936  func (e *environ) jujuGroupName() string {
   937  	return fmt.Sprintf("juju-%s", e.name)
   938  }
   939  
   940  func (e *environ) machineFullName(machineId string) string {
   941  	return fmt.Sprintf("juju-%s-%s", e.Name(), names.MachineTag(machineId))
   942  }
   943  
   944  // machinesFilter returns a nova.Filter matching all machines in the environment.
   945  func (e *environ) machinesFilter() *nova.Filter {
   946  	filter := nova.NewFilter()
   947  	filter.Set(nova.FilterServer, fmt.Sprintf("juju-%s-machine-\\d*", e.Name()))
   948  	return filter
   949  }
   950  
   951  func (e *environ) openPortsInGroup(name string, ports []instance.Port) error {
   952  	novaclient := e.nova()
   953  	group, err := novaclient.SecurityGroupByName(name)
   954  	if err != nil {
   955  		return err
   956  	}
   957  	for _, port := range ports {
   958  		_, err := novaclient.CreateSecurityGroupRule(nova.RuleInfo{
   959  			ParentGroupId: group.Id,
   960  			FromPort:      port.Number,
   961  			ToPort:        port.Number,
   962  			IPProtocol:    port.Protocol,
   963  			Cidr:          "0.0.0.0/0",
   964  		})
   965  		if err != nil {
   966  			// TODO: if err is not rule already exists, raise?
   967  			logger.Debugf("error creating security group rule: %v", err.Error())
   968  		}
   969  	}
   970  	return nil
   971  }
   972  
   973  func (e *environ) closePortsInGroup(name string, ports []instance.Port) error {
   974  	if len(ports) == 0 {
   975  		return nil
   976  	}
   977  	novaclient := e.nova()
   978  	group, err := novaclient.SecurityGroupByName(name)
   979  	if err != nil {
   980  		return err
   981  	}
   982  	// TODO: Hey look ma, it's quadratic
   983  	for _, port := range ports {
   984  		for _, p := range (*group).Rules {
   985  			if p.IPProtocol == nil || *p.IPProtocol != port.Protocol ||
   986  				p.FromPort == nil || *p.FromPort != port.Number ||
   987  				p.ToPort == nil || *p.ToPort != port.Number {
   988  				continue
   989  			}
   990  			err := novaclient.DeleteSecurityGroupRule(p.Id)
   991  			if err != nil {
   992  				return err
   993  			}
   994  			break
   995  		}
   996  	}
   997  	return nil
   998  }
   999  
  1000  func (e *environ) portsInGroup(name string) (ports []instance.Port, err error) {
  1001  	group, err := e.nova().SecurityGroupByName(name)
  1002  	if err != nil {
  1003  		return nil, err
  1004  	}
  1005  	for _, p := range (*group).Rules {
  1006  		for i := *p.FromPort; i <= *p.ToPort; i++ {
  1007  			ports = append(ports, instance.Port{
  1008  				Protocol: *p.IPProtocol,
  1009  				Number:   i,
  1010  			})
  1011  		}
  1012  	}
  1013  	instance.SortPorts(ports)
  1014  	return ports, nil
  1015  }
  1016  
  1017  // TODO: following 30 lines nearly verbatim from environs/ec2
  1018  
  1019  func (e *environ) OpenPorts(ports []instance.Port) error {
  1020  	if e.Config().FirewallMode() != config.FwGlobal {
  1021  		return fmt.Errorf("invalid firewall mode %q for opening ports on environment",
  1022  			e.Config().FirewallMode())
  1023  	}
  1024  	if err := e.openPortsInGroup(e.globalGroupName(), ports); err != nil {
  1025  		return err
  1026  	}
  1027  	logger.Infof("opened ports in global group: %v", ports)
  1028  	return nil
  1029  }
  1030  
  1031  func (e *environ) ClosePorts(ports []instance.Port) error {
  1032  	if e.Config().FirewallMode() != config.FwGlobal {
  1033  		return fmt.Errorf("invalid firewall mode %q for closing ports on environment",
  1034  			e.Config().FirewallMode())
  1035  	}
  1036  	if err := e.closePortsInGroup(e.globalGroupName(), ports); err != nil {
  1037  		return err
  1038  	}
  1039  	logger.Infof("closed ports in global group: %v", ports)
  1040  	return nil
  1041  }
  1042  
  1043  func (e *environ) Ports() ([]instance.Port, error) {
  1044  	if e.Config().FirewallMode() != config.FwGlobal {
  1045  		return nil, fmt.Errorf("invalid firewall mode %q for retrieving ports from environment",
  1046  			e.Config().FirewallMode())
  1047  	}
  1048  	return e.portsInGroup(e.globalGroupName())
  1049  }
  1050  
  1051  func (e *environ) Provider() environs.EnvironProvider {
  1052  	return &providerInstance
  1053  }
  1054  
  1055  func (e *environ) setUpGlobalGroup(groupName string, statePort, apiPort int) (nova.SecurityGroup, error) {
  1056  	return e.ensureGroup(groupName,
  1057  		[]nova.RuleInfo{
  1058  			{
  1059  				IPProtocol: "tcp",
  1060  				FromPort:   22,
  1061  				ToPort:     22,
  1062  				Cidr:       "0.0.0.0/0",
  1063  			},
  1064  			{
  1065  				IPProtocol: "tcp",
  1066  				FromPort:   statePort,
  1067  				ToPort:     statePort,
  1068  				Cidr:       "0.0.0.0/0",
  1069  			},
  1070  			{
  1071  				IPProtocol: "tcp",
  1072  				FromPort:   apiPort,
  1073  				ToPort:     apiPort,
  1074  				Cidr:       "0.0.0.0/0",
  1075  			},
  1076  			{
  1077  				IPProtocol: "tcp",
  1078  				FromPort:   1,
  1079  				ToPort:     65535,
  1080  			},
  1081  			{
  1082  				IPProtocol: "udp",
  1083  				FromPort:   1,
  1084  				ToPort:     65535,
  1085  			},
  1086  			{
  1087  				IPProtocol: "icmp",
  1088  				FromPort:   -1,
  1089  				ToPort:     -1,
  1090  			},
  1091  		})
  1092  }
  1093  
  1094  // setUpGroups creates the security groups for the new machine, and
  1095  // returns them.
  1096  //
  1097  // Instances are tagged with a group so they can be distinguished from
  1098  // other instances that might be running on the same OpenStack account.
  1099  // In addition, a specific machine security group is created for each
  1100  // machine, so that its firewall rules can be configured per machine.
  1101  //
  1102  // Note: ideally we'd have a better way to determine group membership so that 2
  1103  // people that happen to share an openstack account and name their environment
  1104  // "openstack" don't end up destroying each other's machines.
  1105  func (e *environ) setUpGroups(machineId string, statePort, apiPort int) ([]nova.SecurityGroup, error) {
  1106  	jujuGroup, err := e.setUpGlobalGroup(e.jujuGroupName(), statePort, apiPort)
  1107  	if err != nil {
  1108  		return nil, err
  1109  	}
  1110  	var machineGroup nova.SecurityGroup
  1111  	switch e.Config().FirewallMode() {
  1112  	case config.FwInstance:
  1113  		machineGroup, err = e.ensureGroup(e.machineGroupName(machineId), nil)
  1114  	case config.FwGlobal:
  1115  		machineGroup, err = e.ensureGroup(e.globalGroupName(), nil)
  1116  	}
  1117  	if err != nil {
  1118  		return nil, err
  1119  	}
  1120  	groups := []nova.SecurityGroup{jujuGroup, machineGroup}
  1121  	if e.ecfg().useDefaultSecurityGroup() {
  1122  		defaultGroup, err := e.nova().SecurityGroupByName("default")
  1123  		if err != nil {
  1124  			return nil, fmt.Errorf("loading default security group: %v", err)
  1125  		}
  1126  		groups = append(groups, *defaultGroup)
  1127  	}
  1128  	return groups, nil
  1129  }
  1130  
  1131  // zeroGroup holds the zero security group.
  1132  var zeroGroup nova.SecurityGroup
  1133  
  1134  // ensureGroup returns the security group with name and perms.
  1135  // If a group with name does not exist, one will be created.
  1136  // If it exists, its permissions are set to perms.
  1137  func (e *environ) ensureGroup(name string, rules []nova.RuleInfo) (nova.SecurityGroup, error) {
  1138  	novaClient := e.nova()
  1139  	// First attempt to look up an existing group by name.
  1140  	group, err := novaClient.SecurityGroupByName(name)
  1141  	if err == nil {
  1142  		// Group exists, so assume it is correctly set up and return it.
  1143  		// TODO(jam): 2013-09-18 http://pad.lv/121795
  1144  		// We really should verify the group is set up correctly,
  1145  		// because deleting and re-creating environments can get us bad
  1146  		// groups (especially if they were set up under Python)
  1147  		return *group, nil
  1148  	}
  1149  	// Doesn't exist, so try and create it.
  1150  	group, err = novaClient.CreateSecurityGroup(name, "juju group")
  1151  	if err != nil {
  1152  		if !gooseerrors.IsDuplicateValue(err) {
  1153  			return zeroGroup, err
  1154  		} else {
  1155  			// We just tried to create a duplicate group, so load the existing group.
  1156  			group, err = novaClient.SecurityGroupByName(name)
  1157  			if err != nil {
  1158  				return zeroGroup, err
  1159  			}
  1160  			return *group, nil
  1161  		}
  1162  	}
  1163  	// The new group is created so now add the rules.
  1164  	group.Rules = make([]nova.SecurityGroupRule, len(rules))
  1165  	for i, rule := range rules {
  1166  		rule.ParentGroupId = group.Id
  1167  		if rule.Cidr == "" {
  1168  			// http://pad.lv/1226996 Rules that don't have a CIDR
  1169  			// are meant to apply only to this group. If you don't
  1170  			// supply CIDR or GroupId then openstack assumes you
  1171  			// mean CIDR=0.0.0.0/0
  1172  			rule.GroupId = &group.Id
  1173  		}
  1174  		groupRule, err := novaClient.CreateSecurityGroupRule(rule)
  1175  		if err != nil && !gooseerrors.IsDuplicateValue(err) {
  1176  			return zeroGroup, err
  1177  		}
  1178  		group.Rules[i] = *groupRule
  1179  	}
  1180  	return *group, nil
  1181  }
  1182  
  1183  func (e *environ) terminateInstances(ids []instance.Id) error {
  1184  	if len(ids) == 0 {
  1185  		return nil
  1186  	}
  1187  	var firstErr error
  1188  	novaClient := e.nova()
  1189  	for _, id := range ids {
  1190  		err := novaClient.DeleteServer(string(id))
  1191  		if gooseerrors.IsNotFound(err) {
  1192  			err = nil
  1193  		}
  1194  		if err != nil && firstErr == nil {
  1195  			logger.Debugf("error terminating instance %q: %v", id, err)
  1196  			firstErr = err
  1197  		}
  1198  	}
  1199  	return firstErr
  1200  }
  1201  
  1202  // MetadataLookupParams returns parameters which are used to query simplestreams metadata.
  1203  func (e *environ) MetadataLookupParams(region string) (*simplestreams.MetadataLookupParams, error) {
  1204  	if region == "" {
  1205  		region = e.ecfg().region()
  1206  	}
  1207  	return &simplestreams.MetadataLookupParams{
  1208  		Series:        e.ecfg().DefaultSeries(),
  1209  		Region:        region,
  1210  		Endpoint:      e.ecfg().authURL(),
  1211  		Architectures: []string{"amd64", "arm"},
  1212  	}, nil
  1213  }
  1214  
  1215  // Region is specified in the HasRegion interface.
  1216  func (e *environ) Region() (simplestreams.CloudSpec, error) {
  1217  	return simplestreams.CloudSpec{
  1218  		Region:   e.ecfg().region(),
  1219  		Endpoint: e.ecfg().authURL(),
  1220  	}, nil
  1221  }