// SeriesImage gets an instances.Image for the specified series, image stream // and location. The resulting Image's ID is in the URN format expected by // Azure Resource Manager. // // For Ubuntu, we query the SKUs to determine the most recent point release // for a series. func SeriesImage( series, stream, location string, client compute.VirtualMachineImagesClient, ) (*instances.Image, error) { seriesOS, err := jujuseries.GetOSFromSeries(series) if err != nil { return nil, errors.Trace(err) } var publisher, offering, sku string switch seriesOS { case os.Ubuntu: publisher = ubuntuPublisher offering = ubuntuOffering sku, err = ubuntuSKU(series, stream, location, client) if err != nil { return nil, errors.Annotatef(err, "selecting SKU for %s", series) } case os.Windows: publisher = windowsPublisher offering = windowsOffering switch series { case "win2012": sku = "2012-Datacenter" case "win2012r2": sku = "2012-R2-Datacenter" default: return nil, errors.NotSupportedf("deploying %s", series) } case os.CentOS: publisher = centOSPublisher offering = centOSOffering switch series { case "centos7": sku = "7.1" default: return nil, errors.NotSupportedf("deploying %s", series) } default: // TODO(axw) CentOS return nil, errors.NotSupportedf("deploying %s", seriesOS) } return &instances.Image{ Id: fmt.Sprintf("%s:%s:%s:latest", publisher, offering, sku), Arch: arch.AMD64, VirtType: "Hyper-V", }, nil }
// Validate checks the service for invalid values. func (s *Service) Validate() error { if err := s.Service.Validate(renderer); err != nil { return errors.Trace(err) } if s.Service.Conf.Transient { return errors.NotSupportedf("transient services") } if s.Service.Conf.AfterStopped != "" { return errors.NotSupportedf("Conf.AfterStopped") } return nil }
// AssertProviderCredentialsValid asserts that the given provider is // able to validate the given authentication type and credential // attributes; and that removing any one of the attributes will cause // the validation to fail. func AssertProviderCredentialsValid(c *gc.C, p environs.EnvironProvider, authType cloud.AuthType, attrs map[string]string) { schema, ok := p.CredentialSchemas()[authType] c.Assert(ok, jc.IsTrue, gc.Commentf("missing schema for %q auth-type", authType)) validate := func(attrs map[string]string) error { _, err := schema.Finalize(attrs, func(string) ([]byte, error) { return nil, errors.NotSupportedf("reading files") }) return err } err := validate(attrs) c.Assert(err, jc.ErrorIsNil) for excludedKey := range attrs { field, _ := schema.Attribute(excludedKey) if field.Optional { continue } reducedAttrs := make(map[string]string) for key, value := range attrs { if key != excludedKey { reducedAttrs[key] = value } } err := validate(reducedAttrs) if field.FileAttr != "" { c.Assert(err, gc.ErrorMatches, fmt.Sprintf( `either %q or %q must be specified`, excludedKey, field.FileAttr), ) } else { c.Assert(err, gc.ErrorMatches, excludedKey+": expected string, got nothing") } } }
// parseJSONKey extracts the auth information from the JSON file // downloaded from the GCE console (under /apiui/credential). func parseJSONKey(jsonKey []byte) (map[string]string, error) { in := make(map[string]string) if err := json.Unmarshal(jsonKey, &in); err != nil { return nil, errors.Trace(err) } keyType, ok := in["type"] if !ok { return nil, errors.New(`missing "type"`) } switch keyType { case jsonKeyTypeServiceAccount: out := make(map[string]string) for k, v := range in { switch k { case "private_key": out[OSEnvPrivateKey] = v case "client_email": out[OSEnvClientEmail] = v case "client_id": out[OSEnvClientID] = v case "project_id": out[OSEnvProjectID] = v } } return out, nil default: return nil, errors.NotSupportedf("JSON key type %q", keyType) } }
// getKeystoneToolsSource is a tools.ToolsDataSourceFunc that // returns a DataSource using the "juju-tools" keystone URL. func getKeystoneToolsSource(env environs.Environ) (simplestreams.DataSource, error) { e, ok := env.(*Environ) if !ok { return nil, errors.NotSupportedf("non-openstack model") } return e.getKeystoneDataSource(&e.keystoneToolsDataSourceMutex, &e.keystoneToolsDataSource, "juju-tools") }
// AuthToken returns a service principal token, suitable for authorizing // Resource Manager API requests, based on the supplied CloudSpec. func AuthToken(cloud environs.CloudSpec, sender autorest.Sender) (*azure.ServicePrincipalToken, error) { if authType := cloud.Credential.AuthType(); authType != clientCredentialsAuthType { // We currently only support a single auth-type for // non-interactive authentication. Interactive auth // is used only to generate a service-principal. return nil, errors.NotSupportedf("auth-type %q", authType) } credAttrs := cloud.Credential.Attributes() subscriptionId := credAttrs[credAttrSubscriptionId] appId := credAttrs[credAttrAppId] appPassword := credAttrs[credAttrAppPassword] client := subscriptions.Client{subscriptions.NewWithBaseURI(cloud.Endpoint)} client.Sender = sender oauthConfig, _, err := azureauth.OAuthConfig(client, cloud.Endpoint, subscriptionId) if err != nil { return nil, errors.Trace(err) } resource := azureauth.TokenResource(cloud.Endpoint) token, err := azure.NewServicePrincipalToken( *oauthConfig, appId, appPassword, resource, ) if err != nil { return nil, errors.Annotate(err, "constructing service principal token") } if sender != nil { token.SetSender(sender) } return token, nil }
func (s *SubnetsSuite) TestAllZonesWithNoBackingZonesAndSetFails(c *gc.C) { apiservertesting.BackingInstance.SetUp(c, apiservertesting.StubZonedEnvironName, apiservertesting.WithoutZones, apiservertesting.WithSpaces, apiservertesting.WithSubnets) apiservertesting.SharedStub.SetErrors( nil, // Backing.AvailabilityZones nil, // Backing.EnvironConfig nil, // Provider.Open nil, // ZonedEnviron.AvailabilityZones errors.NotSupportedf("setting"), // Backing.SetAvailabilityZones ) results, err := s.facade.AllZones() c.Assert(err, gc.ErrorMatches, `cannot update known zones: setting not supported`, ) // Verify the cause is not obscured. c.Assert(err, jc.Satisfies, errors.IsNotSupported) c.Assert(results, jc.DeepEquals, params.ZoneResults{}) apiservertesting.CheckMethodCalls(c, apiservertesting.SharedStub, apiservertesting.BackingCall("AvailabilityZones"), apiservertesting.BackingCall("EnvironConfig"), apiservertesting.ProviderCall("Open", apiservertesting.BackingInstance.EnvConfig), apiservertesting.ZonedEnvironCall("AvailabilityZones"), apiservertesting.BackingCall("SetAvailabilityZones", apiservertesting.ProviderInstance.Zones), ) }
// AllocateAddress requests an address to be allocated for the // given instance on the given subnet. func (env *environ) AllocateAddress(instId instance.Id, subnetId network.Id, addr network.Address, macAddress, hostname string) error { if !environs.AddressAllocationEnabled() { return errors.NotSupportedf("address allocation") } if err := env.checkBroken("AllocateAddress"); err != nil { return err } estate, err := env.state() if err != nil { return err } estate.mu.Lock() defer estate.mu.Unlock() estate.maxAddr++ estate.ops <- OpAllocateAddress{ Env: env.name, InstanceId: instId, SubnetId: subnetId, Address: addr, MACAddress: macAddress, HostName: hostname, } return nil }
// NetworkGet returns the specified network's configuration. func (c *networkClient) NetworkGet(name string) (shared.NetworkConfig, error) { if !c.supported { return shared.NetworkConfig{}, errors.NotSupportedf("network API not supported on this remote") } return c.raw.NetworkGet(name) }
// WatchDebugLog returns a ReadCloser that the caller can read the log // lines from. Only log lines that match the filtering specified in // the DebugLogParams are returned. It returns an error that satisfies // errors.IsNotImplemented when the API server does not support the // end-point. // // TODO(dimitern) We already have errors.IsNotImplemented - why do we // need to define a different error for this purpose here? func (c *Client) WatchDebugLog(args DebugLogParams) (io.ReadCloser, error) { // The websocket connection just hangs if the server doesn't have the log // end point. So do a version check, as version was added at the same time // as the remote end point. _, err := c.AgentVersion() if err != nil { return nil, errors.NotSupportedf("WatchDebugLog") } // Prepare URL query attributes. attrs := url.Values{ "includeEntity": args.IncludeEntity, "includeModule": args.IncludeModule, "excludeEntity": args.ExcludeEntity, "excludeModule": args.ExcludeModule, } if args.Replay { attrs.Set("replay", fmt.Sprint(args.Replay)) } if args.Limit > 0 { attrs.Set("maxLines", fmt.Sprint(args.Limit)) } if args.Backlog > 0 { attrs.Set("backlog", fmt.Sprint(args.Backlog)) } if args.Level != loggo.UNSPECIFIED { attrs.Set("level", fmt.Sprint(args.Level)) } connection, err := c.st.ConnectStream("/log", attrs) if err != nil { return nil, errors.Trace(err) } return connection, nil }
// BootstrapConfig is specified in the EnvironProvider interface. func (p maasEnvironProvider) BootstrapConfig(args environs.BootstrapConfigParams) (*config.Config, error) { // For MAAS, either: // 1. the endpoint from the cloud definition defines the MAAS server URL // (if a full cloud definition had been set up) // 2. the region defines the MAAS server ip/host // (if the bootstrap shortcut is used) server := args.CloudEndpoint if server == "" && args.CloudRegion != "" { server = fmt.Sprintf("http://%s/MAAS", args.CloudRegion) } if server == "" { return nil, errors.New("MAAS server not specified") } attrs := map[string]interface{}{ "maas-server": server, } // Add the credentials. switch authType := args.Credentials.AuthType(); authType { case cloud.OAuth1AuthType: credentialAttrs := args.Credentials.Attributes() for k, v := range credentialAttrs { attrs[k] = v } default: return nil, errors.NotSupportedf("%q auth-type", authType) } cfg, err := args.Config.Apply(attrs) if err != nil { return nil, errors.Trace(err) } return p.PrepareForCreateEnvironment(cfg) }
// SetStatus updates the status of the named service. func (f *FakeServiceData) SetStatus(name, status string) error { f.mu.Lock() defer f.mu.Unlock() if status == "" { f.managedNames.Remove(name) f.installedNames.Remove(name) f.runningNames.Remove(name) return nil } managed := true if strings.HasPrefix(status, "(") && strings.HasSuffix(status, ")") { status = status[1 : len(status)-1] managed = false } switch status { case "installed": f.installedNames.Add(name) f.runningNames.Remove(name) case "running": f.installedNames.Add(name) f.runningNames.Add(name) default: return errors.NotSupportedf("status %q", status) } if managed { f.managedNames.Add(name) } return nil }
// NewWorker starts an http server listening on an abstract domain socket // which will be created with the specified name. func NewWorker(config Config) (worker.Worker, error) { if err := config.Validate(); err != nil { return nil, errors.Trace(err) } if runtime.GOOS != "linux" { return nil, errors.NotSupportedf("os %q", runtime.GOOS) } path := "@" + config.SocketName addr, err := net.ResolveUnixAddr("unix", path) if err != nil { return nil, errors.Annotate(err, "unable to resolve unix socket") } l, err := net.ListenUnix("unix", addr) if err != nil { return nil, errors.Annotate(err, "unable to listen on unix socket") } logger.Debugf("introspection worker listening on %q", path) w := &socketListener{ listener: l, reporter: config.Reporter, done: make(chan struct{}), } go w.serve() go w.run() return w, nil }
// BootstrapConfig is specified in the EnvironProvider interface. func (p environProvider) BootstrapConfig(args environs.BootstrapConfigParams) (*config.Config, error) { // Add credentials to the configuration. attrs := map[string]interface{}{ "region": args.CloudRegion, // TODO(axw) stop relying on hard-coded // region endpoint information // in the provider, and use // args.CloudEndpoint here. } switch authType := args.Credentials.AuthType(); authType { case cloud.AccessKeyAuthType: credentialAttrs := args.Credentials.Attributes() attrs["access-key"] = credentialAttrs["access-key"] attrs["secret-key"] = credentialAttrs["secret-key"] default: return nil, errors.NotSupportedf("%q auth-type", authType) } // Set the default block-storage source. if _, ok := args.Config.StorageDefaultBlockSource(); !ok { attrs[config.StorageDefaultBlockSourceKey] = EBS_ProviderType } cfg, err := args.Config.Apply(attrs) if err != nil { return nil, errors.Trace(err) } return p.PrepareForCreateEnvironment(cfg) }
// BootstrapConfig is specified in the EnvironProvider interface. func (prov *azureEnvironProvider) BootstrapConfig(args environs.BootstrapConfigParams) (*config.Config, error) { // Ensure that internal configuration is not specified, and then set // what we can now. We only need to do this during bootstrap. Validate // will check for changes later. unknownAttrs := args.Config.UnknownAttrs() for _, key := range internalConfigAttributes { if _, ok := unknownAttrs[key]; ok { return nil, errors.Errorf(`internal config %q must not be specified`, key) } } attrs := map[string]interface{}{ configAttrLocation: args.CloudRegion, configAttrEndpoint: args.CloudEndpoint, configAttrStorageEndpoint: args.CloudStorageEndpoint, } switch authType := args.Credentials.AuthType(); authType { case cloud.UserPassAuthType: for k, v := range args.Credentials.Attributes() { attrs[k] = v } default: return nil, errors.NotSupportedf("%q auth-type", authType) } cfg, err := args.Config.Apply(attrs) if err != nil { return nil, errors.Annotate(err, "updating config") } return cfg, nil }
// FilesystemSource is defined on storage.Provider. func (p *StorageProvider) FilesystemSource(environConfig *config.Config, providerConfig *storage.Config) (storage.FilesystemSource, error) { p.MethodCall(p, "FilesystemSource", environConfig, providerConfig) if p.FilesystemSourceFunc != nil { return p.FilesystemSourceFunc(environConfig, providerConfig) } return nil, errors.NotSupportedf("filesystems") }
// NetworkCreate creates the specified network. func (c *networkClient) NetworkCreate(name string, config map[string]string) error { if !c.supported { return errors.NotSupportedf("network API not supported on this remote") } return c.raw.NetworkCreate(name, config) }
// parseJSONKey extracts the auth information from the JSON file // downloaded from the GCE console (under /apiui/credential). func parseJSONKey(jsonKey []byte) (map[string]string, error) { data := make(map[string]string) if err := json.Unmarshal(jsonKey, &data); err != nil { return nil, errors.Trace(err) } keyType, ok := data["type"] if !ok { return nil, errors.New(`missing "type"`) } switch keyType { case jsonKeyTypeServiceAccount: for k, v := range data { switch k { case "private_key": data[OSEnvPrivateKey] = v delete(data, k) case "client_email": data[OSEnvClientEmail] = v delete(data, k) case "client_id": data[OSEnvClientID] = v delete(data, k) } } default: return nil, errors.NotSupportedf("JSON key type %q", data["type"]) } return data, nil }
// InstanceAvailabilityZoneNames implements environs.ZonedEnviron. func (env *environ) InstanceAvailabilityZoneNames(ids []instance.Id) ([]string, error) { // TODO(dimitern): Fix this properly. if err := env.checkBroken("InstanceAvailabilityZoneNames"); err != nil { return nil, errors.NotSupportedf("instance availability zones") } return []string{"zone1"}, nil }
func (v *azureVolumeSource) attachVolume( p storage.VolumeAttachmentParams, role *gwacl.PersistentVMRole, ) (*storage.VolumeAttachment, error) { var disks []gwacl.DataVirtualHardDisk if role.DataVirtualHardDisks != nil { disks = *role.DataVirtualHardDisks } // Check if the disk is already attached to the machine. mediaLinkPrefix := v.vhdMediaLinkPrefix() for _, disk := range disks { if !strings.HasPrefix(disk.MediaLink, mediaLinkPrefix) { continue } _, volumeId := path.Split(disk.MediaLink) if volumeId != p.VolumeId { continue } return &storage.VolumeAttachment{ p.Volume, p.Machine, storage.VolumeAttachmentInfo{ BusAddress: diskBusAddress(disk.LUN), }, }, nil } // If the disk is not attached already, the AttachVolumes call must // fail. We do not support persistent volumes at the moment, and if // we get here it means that the disk has been detached out of band. return nil, errors.NotSupportedf("attaching volumes") }
func getImageSource(env environs.Environ) (simplestreams.DataSource, error) { e, ok := env.(*environ) if !ok { return nil, errors.NotSupportedf("non-cloudsigma environment") } return simplestreams.NewURLDataSource("cloud images", fmt.Sprintf(CloudsigmaCloudImagesURLTemplate, e.ecfg.region()), utils.VerifySSLHostnames), nil }
func (s *userManagerSuite) SetUpTest(c *gc.C) { s.JujuConnSuite.SetUpTest(c) s.createLocalLoginMacaroon = func(tag names.UserTag) (*macaroon.Macaroon, error) { return nil, errors.NotSupportedf("CreateLocalLoginMacaroon") } s.resources = common.NewResources() s.resources.RegisterNamed("createLocalLoginMacaroon", common.ValueResource{ func(tag names.UserTag) (*macaroon.Macaroon, error) { return s.createLocalLoginMacaroon(tag) }, }) adminTag := s.AdminUserTag(c) s.adminName = adminTag.Name() s.authorizer = apiservertesting.FakeAuthorizer{ Tag: adminTag, } var err error s.usermanager, err = usermanager.NewUserManagerAPI(s.State, s.resources, s.authorizer) c.Assert(err, jc.ErrorIsNil) s.BlockHelper = commontesting.NewBlockHelper(s.APIState) s.AddCleanup(func(*gc.C) { s.BlockHelper.Close() }) }
// FinalizeCredential is part of the environs.ProviderCredentials interface. func (c environProviderCredentials) FinalizeCredential( ctx environs.FinalizeCredentialContext, args environs.FinalizeCredentialParams, ) (*cloud.Credential, error) { switch authType := args.Credential.AuthType(); authType { case deviceCodeAuthType: subscriptionId := args.Credential.Attributes()[credAttrSubscriptionId] applicationId, password, err := c.interactiveCreateServicePrincipal( ctx.GetStderr(), c.sender, c.requestInspector, args.CloudEndpoint, args.CloudIdentityEndpoint, subscriptionId, clock.WallClock, utils.NewUUID, ) if err != nil { return nil, errors.Trace(err) } out := cloud.NewCredential(clientCredentialsAuthType, map[string]string{ credAttrSubscriptionId: subscriptionId, credAttrAppId: applicationId, credAttrAppPassword: password, }) out.Label = args.Credential.Label return &out, nil case clientCredentialsAuthType: return &args.Credential, nil default: return nil, errors.NotSupportedf("%q auth-type", authType) } }
// vmExtension creates a CustomScript VM extension for the given VM // which will execute the CustomData on the machine as a script. func vmExtensionProperties(os jujuos.OSType) (*compute.VirtualMachineExtensionProperties, error) { var commandToExecute, extensionPublisher, extensionType, extensionVersion string switch os { case jujuos.Windows: commandToExecute = windowsExecuteCustomScriptCommand extensionPublisher = windowsCustomScriptPublisher extensionType = windowsCustomScriptType extensionVersion = windowsCustomScriptVersion case jujuos.CentOS: commandToExecute = linuxExecuteCustomScriptCommand extensionPublisher = linuxCustomScriptPublisher extensionType = linuxCustomScriptType extensionVersion = linuxCustomScriptVersion default: // Ubuntu renders CustomData as cloud-config, and interprets // it with cloud-init. Windows and CentOS do not use cloud-init // on Azure. return nil, errors.NotSupportedf("CustomScript extension for OS %q", os) } extensionSettings := map[string]interface{}{ "commandToExecute": commandToExecute, } return &compute.VirtualMachineExtensionProperties{ Publisher: to.StringPtr(extensionPublisher), Type: to.StringPtr(extensionType), TypeHandlerVersion: to.StringPtr(extensionVersion), AutoUpgradeMinorVersion: to.BoolPtr(true), Settings: &extensionSettings, }, nil }
// BootstrapConfig is specified in the EnvironProvider interface. func (p EnvironProvider) BootstrapConfig(args environs.BootstrapConfigParams) (*config.Config, error) { // Add credentials to the configuration. attrs := map[string]interface{}{ "region": args.CloudRegion, "auth-url": args.CloudEndpoint, } credentialAttrs := args.Credentials.Attributes() switch authType := args.Credentials.AuthType(); authType { case cloud.UserPassAuthType: // TODO(axw) we need a way of saying to use legacy auth. attrs["username"] = credentialAttrs["username"] attrs["password"] = credentialAttrs["password"] attrs["tenant-name"] = credentialAttrs["tenant-name"] attrs["domain-name"] = credentialAttrs["domain-name"] attrs["auth-mode"] = AuthUserPass case cloud.AccessKeyAuthType: attrs["access-key"] = credentialAttrs["access-key"] attrs["secret-key"] = credentialAttrs["secret-key"] attrs["tenant-name"] = credentialAttrs["tenant-name"] attrs["auth-mode"] = AuthKeyPair default: return nil, errors.NotSupportedf("%q auth-type", authType) } // Set the default block-storage source. if _, ok := args.Config.StorageDefaultBlockSource(); !ok { attrs[config.StorageDefaultBlockSourceKey] = CinderProviderType } cfg, err := args.Config.Apply(attrs) if err != nil { return nil, errors.Trace(err) } return p.PrepareForCreateEnvironment(cfg) }
// prepareContainerAccessEnvironment retrieves the environment, host machine, and access // for working with containers. func (p *ProvisionerAPI) prepareContainerAccessEnvironment() (environs.NetworkingEnviron, *state.Machine, common.AuthFunc, error) { cfg, err := p.st.EnvironConfig() if err != nil { return nil, nil, nil, errors.Annotate(err, "failed to get environment config") } environ, err := environs.New(cfg) if err != nil { return nil, nil, nil, errors.Annotate(err, "failed to construct an environment from config") } netEnviron, supported := environs.SupportsNetworking(environ) if !supported { // " not supported" will be appended to the message below. return nil, nil, nil, errors.NotSupportedf("environment %q networking", cfg.Name()) } canAccess, err := p.getAuthFunc() if err != nil { return nil, nil, nil, errors.Annotate(err, "cannot authenticate request") } hostAuthTag := p.authorizer.GetAuthTag() if hostAuthTag == nil { return nil, nil, nil, errors.Errorf("authenticated entity tag is nil") } hostTag, err := names.ParseMachineTag(hostAuthTag.String()) if err != nil { return nil, nil, nil, errors.Trace(err) } host, err := p.getMachine(canAccess, hostTag) if err != nil { return nil, nil, nil, errors.Trace(err) } return netEnviron, host, canAccess, nil }
// UserdataConfig is supposed to take in an instanceConfig as well as a // cloudinit.cloudConfig and add attributes in the cloudinit structure based on // the values inside instanceConfig and on the series func NewUserdataConfig(icfg *instancecfg.InstanceConfig, conf cloudinit.CloudConfig) (UserdataConfig, error) { // TODO(ericsnow) bug #1426217 // Protect icfg and conf better. operatingSystem, err := series.GetOSFromSeries(icfg.Series) if err != nil { return nil, err } base := baseConfigure{ tag: names.NewMachineTag(icfg.MachineId), icfg: icfg, conf: conf, os: operatingSystem, } switch operatingSystem { case os.Ubuntu: return &unixConfigure{base}, nil case os.CentOS: return &unixConfigure{base}, nil case os.Windows: return &windowsConfigure{base}, nil default: return nil, errors.NotSupportedf("OS %s", icfg.Series) } }
// VolumeSource is defined on storage.Provider. func (p *StorageProvider) VolumeSource(environConfig *config.Config, providerConfig *storage.Config) (storage.VolumeSource, error) { p.MethodCall(p, "VolumeSource", environConfig, providerConfig) if p.VolumeSourceFunc != nil { return p.VolumeSourceFunc(environConfig, providerConfig) } return nil, errors.NotSupportedf("volumes") }
// NewCredentials returns a new Credentials based on the provided // values. The keys must be recognized OS env var names for the // different credential fields. func NewCredentials(values map[string]string) (*Credentials, error) { var creds Credentials for k, v := range values { switch k { case OSEnvClientID: creds.ClientID = v case OSEnvClientEmail: creds.ClientEmail = v case OSEnvProjectID: creds.ProjectID = v case OSEnvPrivateKey: creds.PrivateKey = []byte(v) default: return nil, errors.NotSupportedf("key %q", k) } } if err := creds.Validate(); err != nil { return nil, errors.Trace(err) } jk, err := creds.buildJSONKey() if err != nil { return nil, errors.Trace(err) } creds.JSONKey = jk return &creds, nil }
// getKeystoneImageSource is an imagemetadata.ImageDataSourceFunc that // returns a DataSource using the "product-streams" keystone URL. func getKeystoneImageSource(env environs.Environ) (simplestreams.DataSource, error) { e, ok := env.(*Environ) if !ok { return nil, errors.NotSupportedf("non-openstack environment") } return e.getKeystoneDataSource(&e.keystoneImageDataSourceMutex, &e.keystoneImageDataSource, "product-streams") }