github.com/niedbalski/juju@v0.0.0-20190215020005-8ff100488e47/provider/lxd/credentials.go (about)

     1  // Copyright 2016 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package lxd
     5  
     6  import (
     7  	"encoding/base64"
     8  	"fmt"
     9  	"io"
    10  	"io/ioutil"
    11  	"net"
    12  	"net/url"
    13  	"os"
    14  	"path/filepath"
    15  
    16  	"github.com/juju/juju/provider/lxd/lxdnames"
    17  
    18  	"github.com/juju/errors"
    19  	"github.com/juju/utils"
    20  	"github.com/lxc/lxd/shared"
    21  	"github.com/lxc/lxd/shared/api"
    22  
    23  	"github.com/juju/juju/cloud"
    24  	"github.com/juju/juju/container/lxd"
    25  	"github.com/juju/juju/environs"
    26  	"github.com/juju/juju/juju/osenv"
    27  )
    28  
    29  const (
    30  	credAttrServerCert    = "server-cert"
    31  	credAttrClientCert    = "client-cert"
    32  	credAttrClientKey     = "client-key"
    33  	credAttrTrustPassword = "trust-password"
    34  )
    35  
    36  // CertificateReadWriter groups methods that is required to read and write
    37  // certificates at a given path.
    38  //go:generate mockgen -package lxd -destination credentials_mock_test.go github.com/juju/juju/provider/lxd CertificateReadWriter,CertificateGenerator,NetLookup
    39  type CertificateReadWriter interface {
    40  	// Read takes a path and returns both a cert and key PEM.
    41  	// Returns an error if there was an issue reading the certs.
    42  	Read(path string) (certPEM, keyPEM []byte, err error)
    43  
    44  	// Write takes a path and cert, key PEM and stores them.
    45  	// Returns an error if there was an issue writing the certs.
    46  	Write(path string, certPEM, keyPEM []byte) error
    47  }
    48  
    49  // CertificateGenerator groups methods for generating a new certificate
    50  type CertificateGenerator interface {
    51  	// Generate creates client or server certificate and key pair,
    52  	// returning them as byte arrays in memory.
    53  	Generate(client bool) (certPEM, keyPEM []byte, err error)
    54  }
    55  
    56  // NetLookup groups methods for looking up hosts and interface addresses.
    57  type NetLookup interface {
    58  
    59  	// LookupHost looks up the given host using the local resolver.
    60  	// It returns a slice of that host's addresses.
    61  	LookupHost(string) ([]string, error)
    62  
    63  	// InterfaceAddrs returns a list of the system's unicast interface
    64  	// addresses.
    65  	InterfaceAddrs() ([]net.Addr, error)
    66  }
    67  
    68  // environProviderCredentials implements environs.ProviderCredentials.
    69  type environProviderCredentials struct {
    70  	certReadWriter  CertificateReadWriter
    71  	certGenerator   CertificateGenerator
    72  	lookup          NetLookup
    73  	serverFactory   ServerFactory
    74  	lxcConfigReader LXCConfigReader
    75  }
    76  
    77  // CredentialSchemas is part of the environs.ProviderCredentials interface.
    78  func (environProviderCredentials) CredentialSchemas() map[cloud.AuthType]cloud.CredentialSchema {
    79  	return map[cloud.AuthType]cloud.CredentialSchema{
    80  		cloud.CertificateAuthType: {
    81  			{
    82  				Name: credAttrServerCert,
    83  				CredentialAttr: cloud.CredentialAttr{
    84  					Description:    "the path to the PEM-encoded LXD server certificate file",
    85  					ExpandFilePath: true,
    86  				},
    87  			}, {
    88  				Name: credAttrClientCert,
    89  				CredentialAttr: cloud.CredentialAttr{
    90  					Description:    "the path to the PEM-encoded LXD client certificate file",
    91  					ExpandFilePath: true,
    92  				},
    93  			}, {
    94  				Name: credAttrClientKey,
    95  				CredentialAttr: cloud.CredentialAttr{
    96  					Description:    "the path to the PEM-encoded LXD client key file",
    97  					ExpandFilePath: true,
    98  				},
    99  			},
   100  		},
   101  		cloud.InteractiveAuthType: {
   102  			{
   103  				Name: credAttrTrustPassword,
   104  				CredentialAttr: cloud.CredentialAttr{
   105  					Description: "the LXD server trust password",
   106  					Hidden:      true,
   107  				},
   108  			},
   109  		},
   110  	}
   111  }
   112  
   113  // RegisterCredentials is part of the environs.ProviderCredentialsRegister interface.
   114  func (p environProviderCredentials) RegisterCredentials(cld cloud.Cloud) (map[string]*cloud.CloudCredential, error) {
   115  	// only register credentials if the operator is attempting to access "lxd"
   116  	// or "localhost"
   117  	cloudName := cld.Name
   118  	if cloudName != lxdnames.DefaultCloud && cloudName != lxdnames.DefaultCloudAltName {
   119  		return make(map[string]*cloud.CloudCredential), nil
   120  	}
   121  
   122  	nopLogf := func(msg string, args ...interface{}) {}
   123  	certPEM, keyPEM, err := p.readOrGenerateCert(nopLogf)
   124  	if err != nil {
   125  		return nil, errors.Trace(err)
   126  	}
   127  
   128  	localCertCredential, err := p.detectLocalCredentials(certPEM, keyPEM)
   129  	if err != nil {
   130  		return nil, errors.Trace(err)
   131  	}
   132  
   133  	return map[string]*cloud.CloudCredential{
   134  		cloudName: {
   135  			DefaultCredential: cloudName,
   136  			AuthCredentials: map[string]cloud.Credential{
   137  				cloudName: *localCertCredential,
   138  			},
   139  		},
   140  	}, nil
   141  }
   142  
   143  // DetectCredentials is part of the environs.ProviderCredentials interface.
   144  func (p environProviderCredentials) DetectCredentials() (*cloud.CloudCredential, error) {
   145  	nopLogf := func(msg string, args ...interface{}) {}
   146  	certPEM, keyPEM, err := p.readOrGenerateCert(nopLogf)
   147  	if err != nil {
   148  		return nil, errors.Trace(err)
   149  	}
   150  
   151  	remoteCertCredentials, err := p.detectRemoteCredentials(certPEM, keyPEM)
   152  	if err != nil {
   153  		logger.Errorf("unable to detect LXC credentials: %s", err)
   154  	}
   155  
   156  	authCredentials := make(map[string]cloud.Credential)
   157  	for k, v := range remoteCertCredentials {
   158  		authCredentials[k] = v
   159  	}
   160  	return &cloud.CloudCredential{
   161  		AuthCredentials: authCredentials,
   162  	}, nil
   163  }
   164  
   165  // detectLocalCredentials will use the local server to read and finalize the
   166  // cloud credentials.
   167  func (p environProviderCredentials) detectLocalCredentials(certPEM, keyPEM []byte) (*cloud.Credential, error) {
   168  	svr, err := p.serverFactory.LocalServer()
   169  	if err != nil {
   170  		return nil, errors.NewNotFound(err, "failed to connect to local LXD")
   171  	}
   172  
   173  	label := fmt.Sprintf("LXD credential %q", lxdnames.DefaultCloud)
   174  	certCredential, err := p.finalizeLocalCredential(
   175  		ioutil.Discard, svr, string(certPEM), string(keyPEM), label,
   176  	)
   177  	return certCredential, errors.Trace(err)
   178  }
   179  
   180  // detectRemoteCredentials will attempt to gather all the potential existing
   181  // remote lxc configurations found in `$HOME/.config/lxc/.config` file.
   182  // Any setups found in the configuration will then be returned as a credential
   183  // that can be automatically loaded into juju.
   184  func (p environProviderCredentials) detectRemoteCredentials(certPEM, keyPEM []byte) (map[string]cloud.Credential, error) {
   185  	configDir := filepath.Join(utils.Home(), ".config", "lxc")
   186  	configPath := filepath.Join(configDir, "config.yml")
   187  	config, err := p.lxcConfigReader.ReadConfig(configPath)
   188  	if err != nil {
   189  		return nil, errors.Trace(err)
   190  	}
   191  
   192  	credentials := make(map[string]cloud.Credential)
   193  	for name, remote := range config.Remotes {
   194  		if remote.Protocol == lxdnames.ProviderType {
   195  			certPath := filepath.Join(configDir, "servercerts", fmt.Sprintf("%s.crt", name))
   196  			serverCert, err := p.lxcConfigReader.ReadCert(certPath)
   197  			if err != nil {
   198  				logger.Errorf("unable to read certificate from %s with error %s", certPath, err)
   199  				continue
   200  			}
   201  			credential := cloud.NewCredential(cloud.CertificateAuthType, map[string]string{
   202  				credAttrClientCert: string(certPEM),
   203  				credAttrClientKey:  string(keyPEM),
   204  				credAttrServerCert: string(serverCert),
   205  			})
   206  			credential.Label = fmt.Sprintf("LXD credential %q", name)
   207  			credentials[name] = credential
   208  		}
   209  	}
   210  	return credentials, nil
   211  }
   212  
   213  func (p environProviderCredentials) readOrGenerateCert(logf func(string, ...interface{})) (certPEM, keyPEM []byte, _ error) {
   214  	// First look in the Juju XDG_DATA dir. This allows the user
   215  	// to explicitly override the certificates used by the lxc
   216  	// client if they wish.
   217  	jujuLXDDir := osenv.JujuXDGDataHomePath("lxd")
   218  	certPEM, keyPEM, err := p.certReadWriter.Read(jujuLXDDir)
   219  	if err == nil {
   220  		logf("Loaded client cert/key from %q", jujuLXDDir)
   221  		return certPEM, keyPEM, nil
   222  	} else if !os.IsNotExist(errors.Cause(err)) {
   223  		return nil, nil, errors.Trace(err)
   224  	}
   225  
   226  	// Next we look in the LXD config dir, in case the user has
   227  	// a client certificate/key pair for use with the "lxc" client
   228  	// application.
   229  	lxdConfigDir := filepath.Join(utils.Home(), ".config", "lxc")
   230  	certPEM, keyPEM, err = p.certReadWriter.Read(lxdConfigDir)
   231  	if err == nil {
   232  		logf("Loaded client cert/key from %q", lxdConfigDir)
   233  		return certPEM, keyPEM, nil
   234  	} else if !os.IsNotExist(errors.Cause(err)) {
   235  		return nil, nil, errors.Trace(err)
   236  	}
   237  
   238  	// No certs were found, so generate one and cache it in the
   239  	// Juju XDG_DATA dir. We cache the certificate so that we
   240  	// avoid uploading a new certificate each time we bootstrap.
   241  	certPEM, keyPEM, err = p.certGenerator.Generate(true)
   242  	if err != nil {
   243  		return nil, nil, errors.Trace(err)
   244  	}
   245  	if err := p.certReadWriter.Write(jujuLXDDir, certPEM, keyPEM); err != nil {
   246  		return nil, nil, errors.Trace(err)
   247  	}
   248  	logf("Generating client cert/key in %q", jujuLXDDir)
   249  	return certPEM, keyPEM, nil
   250  }
   251  
   252  // ShouldFinalizeCredential is part of the environs.RequestFinalizeCredential
   253  // interface.
   254  // This is an optional interface to check if the server certificate has not
   255  // been filled in.
   256  func (p environProviderCredentials) ShouldFinalizeCredential(cred cloud.Credential) bool {
   257  	// The credential is fully formed, so we assume the client
   258  	// certificate is uploaded to the server already.
   259  	credAttrs := cred.Attributes()
   260  	_, ok := credAttrs[credAttrServerCert]
   261  	return !ok
   262  }
   263  
   264  // FinalizeCredential is part of the environs.ProviderCredentials interface.
   265  func (p environProviderCredentials) FinalizeCredential(
   266  	ctx environs.FinalizeCredentialContext,
   267  	args environs.FinalizeCredentialParams,
   268  ) (*cloud.Credential, error) {
   269  	switch authType := args.Credential.AuthType(); authType {
   270  	case cloud.InteractiveAuthType:
   271  		credAttrs := args.Credential.Attributes()
   272  		// We don't care if the password is empty, just that it exists. Empty
   273  		// passwords can be valid ones...
   274  		if _, ok := credAttrs[credAttrTrustPassword]; ok {
   275  			// check to see if the client cert, keys exist, if they do not,
   276  			// generate them for the user.
   277  			if _, ok := getClientCertificates(args.Credential); !ok {
   278  				stderr := ctx.GetStderr()
   279  				nopLogf := func(s string, args ...interface{}) {
   280  					fmt.Fprintf(stderr, s+"\n", args...)
   281  				}
   282  				clientCert, clientKey, err := p.readOrGenerateCert(nopLogf)
   283  				if err != nil {
   284  					return nil, err
   285  				}
   286  
   287  				credAttrs[credAttrClientCert] = string(clientCert)
   288  				credAttrs[credAttrClientKey] = string(clientKey)
   289  
   290  				credential := cloud.NewCredential(cloud.CertificateAuthType, credAttrs)
   291  				credential.Label = args.Credential.Label
   292  
   293  				args.Credential = credential
   294  			}
   295  		}
   296  		fallthrough
   297  	case cloud.CertificateAuthType:
   298  		return p.finalizeCredential(ctx, args)
   299  	default:
   300  		return &args.Credential, nil
   301  	}
   302  }
   303  
   304  func (p environProviderCredentials) finalizeCredential(
   305  	ctx environs.FinalizeCredentialContext,
   306  	args environs.FinalizeCredentialParams,
   307  ) (*cloud.Credential, error) {
   308  	// Credential detection yields a partial certificate containing just
   309  	// the client certificate and key. We check if we have a partial
   310  	// credential, and fill in the server certificate if we can.
   311  	stderr := ctx.GetStderr()
   312  	credAttrs := args.Credential.Attributes()
   313  	// The credential is fully formed, so we assume the client
   314  	// certificate is uploaded to the server already.
   315  	if v, ok := credAttrs[credAttrServerCert]; ok && v != "" {
   316  		return &args.Credential, nil
   317  	}
   318  
   319  	certPEM := credAttrs[credAttrClientCert]
   320  	keyPEM := credAttrs[credAttrClientKey]
   321  	if certPEM == "" {
   322  		return nil, errors.NotValidf("missing or empty %q attribute", credAttrClientCert)
   323  	}
   324  	if keyPEM == "" {
   325  		return nil, errors.NotValidf("missing or empty %q attribute", credAttrClientKey)
   326  	}
   327  
   328  	isLocalEndpoint, err := p.isLocalEndpoint(args.CloudEndpoint)
   329  	if err != nil {
   330  		return nil, errors.Trace(err)
   331  	}
   332  
   333  	// If the end point is local, set up the local server and automate the local
   334  	// certificate credentials.
   335  	if isLocalEndpoint {
   336  		svr, err := p.serverFactory.LocalServer()
   337  		if err != nil {
   338  			return nil, errors.Trace(err)
   339  		}
   340  		cred, err := p.finalizeLocalCredential(
   341  			stderr, svr, certPEM, keyPEM,
   342  			args.Credential.Label,
   343  		)
   344  		return cred, errors.Trace(err)
   345  	}
   346  
   347  	// We're not local, so setup the remote server and automate the remote
   348  	// certificate credentials.
   349  	return p.finalizeRemoteCredential(
   350  		stderr,
   351  		args.CloudEndpoint,
   352  		args.Credential,
   353  	)
   354  }
   355  
   356  func (p environProviderCredentials) finalizeRemoteCredential(
   357  	output io.Writer,
   358  	endpoint string,
   359  	credentials cloud.Credential,
   360  ) (*cloud.Credential, error) {
   361  	clientCert, ok := getClientCertificates(credentials)
   362  	if !ok {
   363  		return nil, errors.NotFoundf("client credentials")
   364  	}
   365  	if err := clientCert.Validate(); err != nil {
   366  		return nil, errors.Annotate(err, "client credentials")
   367  	}
   368  
   369  	credAttrs := credentials.Attributes()
   370  	trustPassword, ok := credAttrs[credAttrTrustPassword]
   371  	if !ok {
   372  		return nil, errors.NotValidf("missing %q attribute", credAttrTrustPassword)
   373  	}
   374  
   375  	insecureCreds := cloud.NewCredential(cloud.CertificateAuthType, credAttrs)
   376  	server, err := p.serverFactory.InsecureRemoteServer(environs.CloudSpec{
   377  		Endpoint:   endpoint,
   378  		Credential: &insecureCreds,
   379  	})
   380  	if err != nil {
   381  		return nil, errors.Trace(err)
   382  	}
   383  
   384  	clientX509Cert, err := clientCert.X509()
   385  	if err != nil {
   386  		return nil, errors.Annotate(err, "client credentials")
   387  	}
   388  
   389  	// check to see if the cert already exists
   390  	fingerprint, err := clientCert.Fingerprint()
   391  	if err != nil {
   392  		return nil, errors.Trace(err)
   393  	}
   394  
   395  	cert, _, err := server.GetCertificate(fingerprint)
   396  	if err != nil || cert == nil {
   397  		if err := server.CreateCertificate(api.CertificatesPost{
   398  			CertificatePut: api.CertificatePut{
   399  				Name: credentials.Label,
   400  				Type: "client",
   401  			},
   402  			Certificate: base64.StdEncoding.EncodeToString(clientX509Cert.Raw),
   403  			Password:    trustPassword,
   404  		}); err != nil {
   405  			return nil, errors.Trace(err)
   406  		}
   407  		fmt.Fprintln(output, "Uploaded certificate to LXD server.")
   408  	} else {
   409  		fmt.Fprintln(output, "Reusing certificate from LXD server.")
   410  	}
   411  
   412  	lxdServer, _, err := server.GetServer()
   413  	if err != nil {
   414  		return nil, errors.Trace(err)
   415  	}
   416  	lxdServerCert := lxdServer.Environment.Certificate
   417  
   418  	// request to make sure that we can actually query correctly in a secure
   419  	// manor.
   420  	attributes := make(map[string]string)
   421  	for k, v := range credAttrs {
   422  		if k == credAttrTrustPassword {
   423  			continue
   424  		}
   425  		attributes[k] = v
   426  	}
   427  	attributes[credAttrServerCert] = lxdServerCert
   428  
   429  	secureCreds := cloud.NewCredential(cloud.CertificateAuthType, attributes)
   430  	server, err = p.serverFactory.RemoteServer(environs.CloudSpec{
   431  		Endpoint:   endpoint,
   432  		Credential: &secureCreds,
   433  	})
   434  	if err != nil {
   435  		return nil, errors.Trace(err)
   436  	}
   437  
   438  	// Store the server's certificate in the credential.
   439  	out := cloud.NewCredential(cloud.CertificateAuthType, map[string]string{
   440  		credAttrClientCert: string(clientCert.CertPEM),
   441  		credAttrClientKey:  string(clientCert.KeyPEM),
   442  		credAttrServerCert: server.ServerCertificate(),
   443  	})
   444  	out.Label = credentials.Label
   445  	return &out, nil
   446  }
   447  
   448  func (p environProviderCredentials) finalizeLocalCredential(
   449  	output io.Writer,
   450  	svr Server,
   451  	certPEM, keyPEM, label string,
   452  ) (*cloud.Credential, error) {
   453  
   454  	// Upload the certificate to the server if necessary.
   455  	clientCert := &lxd.Certificate{
   456  		Name:    "juju",
   457  		CertPEM: []byte(certPEM),
   458  		KeyPEM:  []byte(keyPEM),
   459  	}
   460  	fingerprint, err := clientCert.Fingerprint()
   461  	if err != nil {
   462  		return nil, errors.Trace(err)
   463  	}
   464  	if _, _, err := svr.GetCertificate(fingerprint); lxd.IsLXDNotFound(err) {
   465  		if addCertErr := svr.CreateClientCertificate(clientCert); addCertErr != nil {
   466  			// There is no specific error code returned when
   467  			// attempting to add a certificate that already
   468  			// exists in the database. We can just check
   469  			// again to see if another process added the
   470  			// certificate concurrently with us checking the
   471  			// first time.
   472  			if _, _, err := svr.GetCertificate(fingerprint); lxd.IsLXDNotFound(err) {
   473  				// The cert still isn't there, so report the AddCert error.
   474  				return nil, errors.Annotatef(
   475  					addCertErr, "adding certificate %q", clientCert.Name,
   476  				)
   477  			} else if err != nil {
   478  				return nil, errors.Annotate(err, "querying certificates")
   479  			}
   480  			// The certificate is there now, which implies
   481  			// there was a concurrent AddCert by another
   482  			// process. Carry on.
   483  		}
   484  		fmt.Fprintln(output, "Uploaded certificate to LXD server.")
   485  
   486  	} else if err != nil {
   487  		return nil, errors.Annotate(err, "querying certificates")
   488  	}
   489  
   490  	// Store the server's certificate in the credential.
   491  	out := cloud.NewCredential(cloud.CertificateAuthType, map[string]string{
   492  		credAttrClientCert: certPEM,
   493  		credAttrClientKey:  keyPEM,
   494  		credAttrServerCert: svr.ServerCertificate(),
   495  	})
   496  	out.Label = label
   497  	return &out, nil
   498  }
   499  
   500  func (p environProviderCredentials) isLocalEndpoint(endpoint string) (bool, error) {
   501  	if endpoint == "" {
   502  		// No endpoint specified, so assume we're local. This
   503  		// will happen, for example, when destroying a 2.0
   504  		// LXD controller.
   505  		return true, nil
   506  	}
   507  	endpointURL, err := endpointURL(endpoint)
   508  	if err != nil {
   509  		return false, errors.Trace(err)
   510  	}
   511  	host, _, err := net.SplitHostPort(endpointURL.Host)
   512  	if err != nil {
   513  		host = endpointURL.Host
   514  	}
   515  	endpointAddrs, err := p.lookup.LookupHost(host)
   516  	if err != nil {
   517  		return false, errors.Trace(err)
   518  	}
   519  	localAddrs, err := p.lookup.InterfaceAddrs()
   520  	if err != nil {
   521  		return false, errors.Trace(err)
   522  	}
   523  	return addrsContainsAny(localAddrs, endpointAddrs), nil
   524  }
   525  
   526  // certificateReadWriter is the default implementation for reading and writing
   527  // certificates to disk.
   528  type certificateReadWriter struct{}
   529  
   530  func (certificateReadWriter) Read(path string) ([]byte, []byte, error) {
   531  	clientCertPath := filepath.Join(path, "client.crt")
   532  	clientKeyPath := filepath.Join(path, "client.key")
   533  	certPEM, err := ioutil.ReadFile(clientCertPath)
   534  	if err != nil {
   535  		return nil, nil, errors.Trace(err)
   536  	}
   537  	keyPEM, err := ioutil.ReadFile(clientKeyPath)
   538  	if err != nil {
   539  		return nil, nil, errors.Trace(err)
   540  	}
   541  	return certPEM, keyPEM, nil
   542  }
   543  
   544  func (certificateReadWriter) Write(path string, certPEM, keyPEM []byte) error {
   545  	clientCertPath := filepath.Join(path, "client.crt")
   546  	clientKeyPath := filepath.Join(path, "client.key")
   547  	if err := os.MkdirAll(path, 0700); err != nil {
   548  		return errors.Trace(err)
   549  	}
   550  	if err := ioutil.WriteFile(clientCertPath, certPEM, 0600); err != nil {
   551  		return errors.Trace(err)
   552  	}
   553  	if err := ioutil.WriteFile(clientKeyPath, keyPEM, 0600); err != nil {
   554  		return errors.Trace(err)
   555  	}
   556  	return nil
   557  }
   558  
   559  // certificateGenerator is the default implementation for generating a
   560  // certificate if it's not found on disk.
   561  type certificateGenerator struct{}
   562  
   563  func (certificateGenerator) Generate(client bool) (certPEM, keyPEM []byte, err error) {
   564  	return shared.GenerateMemCert(client)
   565  }
   566  
   567  type netLookup struct{}
   568  
   569  func (netLookup) LookupHost(host string) ([]string, error) {
   570  	return net.LookupHost(host)
   571  }
   572  
   573  func (netLookup) InterfaceAddrs() ([]net.Addr, error) {
   574  	return net.InterfaceAddrs()
   575  }
   576  
   577  func endpointURL(endpoint string) (*url.URL, error) {
   578  	remoteURL, err := url.Parse(endpoint)
   579  	if err != nil || remoteURL.Scheme == "" {
   580  		remoteURL = &url.URL{
   581  			Scheme: "https",
   582  			Host:   endpoint,
   583  		}
   584  	} else {
   585  		// If the user specifies an endpoint, it must be either
   586  		// host:port, or https://host:port. We do not support
   587  		// unix:// endpoints at present.
   588  		if remoteURL.Scheme != "https" {
   589  			return nil, errors.Errorf(
   590  				"invalid URL %q: only HTTPS is supported",
   591  				endpoint,
   592  			)
   593  		}
   594  	}
   595  	return remoteURL, nil
   596  }
   597  
   598  func addrsContainsAny(haystack []net.Addr, needles []string) bool {
   599  	for _, needle := range needles {
   600  		if addrsContains(haystack, needle) {
   601  			return true
   602  		}
   603  	}
   604  	return false
   605  }
   606  
   607  func addrsContains(haystack []net.Addr, needle string) bool {
   608  	ip := net.ParseIP(needle)
   609  	if ip == nil {
   610  		return false
   611  	}
   612  	for _, addr := range haystack {
   613  		if addr, ok := addr.(*net.IPNet); ok && addr.IP.Equal(ip) {
   614  			return true
   615  		}
   616  	}
   617  	return false
   618  }
   619  
   620  func getCertificates(credentials cloud.Credential) (client *lxd.Certificate, server string, ok bool) {
   621  	clientCert, ok := getClientCertificates(credentials)
   622  	if !ok {
   623  		return nil, "", false
   624  	}
   625  	credAttrs := credentials.Attributes()
   626  	serverCertPEM, ok := credAttrs[credAttrServerCert]
   627  	if !ok {
   628  		return nil, "", false
   629  	}
   630  	return clientCert, serverCertPEM, true
   631  }
   632  
   633  func getClientCertificates(credentials cloud.Credential) (client *lxd.Certificate, ok bool) {
   634  	credAttrs := credentials.Attributes()
   635  	clientCertPEM, ok := credAttrs[credAttrClientCert]
   636  	if !ok {
   637  		return nil, false
   638  	}
   639  	clientKeyPEM, ok := credAttrs[credAttrClientKey]
   640  	if !ok {
   641  		return nil, false
   642  	}
   643  	clientCert := &lxd.Certificate{
   644  		Name:    "juju",
   645  		CertPEM: []byte(clientCertPEM),
   646  		KeyPEM:  []byte(clientKeyPEM),
   647  	}
   648  	return clientCert, true
   649  }