github.com/axw/juju@v0.0.0-20161005053422-4bd6544d08d4/api/apiclient.go (about)

     1  // Copyright 2012-2015 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package api
     5  
     6  import (
     7  	"bufio"
     8  	"crypto/tls"
     9  	"crypto/x509"
    10  	"encoding/json"
    11  	"fmt"
    12  	"io"
    13  	"net/http"
    14  	"net/url"
    15  	"strings"
    16  	"sync/atomic"
    17  	"time"
    18  
    19  	"github.com/juju/errors"
    20  	"github.com/juju/loggo"
    21  	"github.com/juju/retry"
    22  	"github.com/juju/utils"
    23  	"github.com/juju/utils/clock"
    24  	"github.com/juju/utils/parallel"
    25  	"github.com/juju/version"
    26  	"golang.org/x/net/websocket"
    27  	"gopkg.in/juju/names.v2"
    28  	"gopkg.in/macaroon-bakery.v1/httpbakery"
    29  	"gopkg.in/macaroon.v1"
    30  
    31  	"github.com/juju/juju/api/base"
    32  	"github.com/juju/juju/apiserver/observer"
    33  	"github.com/juju/juju/apiserver/params"
    34  	"github.com/juju/juju/network"
    35  	"github.com/juju/juju/rpc"
    36  	"github.com/juju/juju/rpc/jsoncodec"
    37  )
    38  
    39  var logger = loggo.GetLogger("juju.api")
    40  
    41  // TODO(fwereade): we should be injecting a Clock; and injecting these values;
    42  // across the board, instead of using these global variables.
    43  var (
    44  	// PingPeriod defines how often the internal connection health check
    45  	// will run.
    46  	PingPeriod = 1 * time.Minute
    47  
    48  	// PingTimeout defines how long a health check can take before we
    49  	// consider it to have failed.
    50  	PingTimeout = 30 * time.Second
    51  )
    52  
    53  type rpcConnection interface {
    54  	Call(req rpc.Request, params, response interface{}) error
    55  	Close() error
    56  }
    57  
    58  // state is the internal implementation of the Connection interface.
    59  type state struct {
    60  	client rpcConnection
    61  	conn   *websocket.Conn
    62  	clock  clock.Clock
    63  
    64  	// addr is the address used to connect to the API server.
    65  	addr string
    66  
    67  	// cookieURL is the URL that HTTP cookies for the API
    68  	// will be associated with (specifically macaroon auth cookies).
    69  	cookieURL *url.URL
    70  
    71  	// modelTag holds the model tag.
    72  	// It is empty if there is no model tag associated with the connection.
    73  	modelTag names.ModelTag
    74  
    75  	// controllerTag holds the controller's tag once we're connected.
    76  	controllerTag names.ControllerTag
    77  
    78  	// serverVersion holds the version of the API server that we are
    79  	// connected to.  It is possible that this version is 0 if the
    80  	// server does not report this during login.
    81  	serverVersion version.Number
    82  
    83  	// hostPorts is the API server addresses returned from Login,
    84  	// which the client may cache and use for failover.
    85  	hostPorts [][]network.HostPort
    86  
    87  	// facadeVersions holds the versions of all facades as reported by
    88  	// Login
    89  	facadeVersions map[string][]int
    90  
    91  	// pingFacadeVersion is the version to use for the pinger. This is lazily
    92  	// set at initialization to avoid a race in our tests. See
    93  	// http://pad.lv/1614732 for more details regarding the race.
    94  	pingerFacadeVersion int
    95  
    96  	// authTag holds the authenticated entity's tag after login.
    97  	authTag names.Tag
    98  
    99  	// mpdelAccess holds the access level of the user to the connected model.
   100  	modelAccess string
   101  
   102  	// controllerAccess holds the access level of the user to the connected controller.
   103  	controllerAccess string
   104  
   105  	// broken is a channel that gets closed when the connection is
   106  	// broken.
   107  	broken chan struct{}
   108  
   109  	// closed is a channel that gets closed when State.Close is called.
   110  	closed chan struct{}
   111  
   112  	// loggedIn holds whether the client has successfully logged
   113  	// in. It's a int32 so that the atomic package can be used to
   114  	// access it safely.
   115  	loggedIn int32
   116  
   117  	// tag, password, macaroons and nonce hold the cached login
   118  	// credentials. These are only valid if loggedIn is 1.
   119  	tag       string
   120  	password  string
   121  	macaroons []macaroon.Slice
   122  	nonce     string
   123  
   124  	// serverRootAddress holds the cached API server address and port used
   125  	// to login.
   126  	serverRootAddress string
   127  
   128  	// serverScheme is the URI scheme of the API Server
   129  	serverScheme string
   130  
   131  	// tlsConfig holds the TLS config appropriate for making SSL
   132  	// connections to the API endpoints.
   133  	tlsConfig *tls.Config
   134  
   135  	// certPool holds the cert pool that is used to authenticate the tls
   136  	// connections to the API.
   137  	certPool *x509.CertPool
   138  
   139  	// bakeryClient holds the client that will be used to
   140  	// authorize macaroon based login requests.
   141  	bakeryClient *httpbakery.Client
   142  }
   143  
   144  // RedirectError is returned from Open when the controller
   145  // needs to inform the client that the model is hosted
   146  // on a different set of API addresses.
   147  type RedirectError struct {
   148  	// Servers holds the sets of addresses of the redirected
   149  	// servers.
   150  	Servers [][]network.HostPort
   151  
   152  	// CACert holds the certificate of the remote server.
   153  	CACert string
   154  }
   155  
   156  func (e *RedirectError) Error() string {
   157  	return fmt.Sprintf("redirection to alternative server required")
   158  }
   159  
   160  // Open establishes a connection to the API server using the Info
   161  // given, returning a State instance which can be used to make API
   162  // requests.
   163  //
   164  // If the model is hosted on a different server, Open
   165  // will return an error with a *RedirectError cause
   166  // holding the details of another server to connect to.
   167  //
   168  // See Connect for details of the connection mechanics.
   169  func Open(info *Info, opts DialOpts) (Connection, error) {
   170  	return open(info, opts, clock.WallClock)
   171  }
   172  
   173  // open is the unexported version of open that also includes
   174  // an explicit clock instance argument.
   175  func open(
   176  	info *Info,
   177  	opts DialOpts,
   178  	clock clock.Clock,
   179  ) (Connection, error) {
   180  	if err := info.Validate(); err != nil {
   181  		return nil, errors.Annotate(err, "validating info for opening an API connection")
   182  	}
   183  	if clock == nil {
   184  		return nil, errors.NotValidf("nil clock")
   185  	}
   186  	conn, tlsConfig, err := connectWebsocket(info, opts)
   187  	if err != nil {
   188  		return nil, errors.Trace(err)
   189  	}
   190  
   191  	client := rpc.NewConn(jsoncodec.NewWebsocket(conn), observer.None())
   192  	client.Start()
   193  
   194  	bakeryClient := opts.BakeryClient
   195  	if bakeryClient == nil {
   196  		bakeryClient = httpbakery.NewClient()
   197  	} else {
   198  		// Make a copy of the bakery client and its HTTP client
   199  		c := *opts.BakeryClient
   200  		bakeryClient = &c
   201  		httpc := *bakeryClient.Client
   202  		bakeryClient.Client = &httpc
   203  	}
   204  	apiHost := conn.Config().Location.Host
   205  	// Technically when there's no CACert, we don't need this
   206  	// machinery, because we could just use http.DefaultTransport
   207  	// for everything, but it's easier just to leave it in place.
   208  	bakeryClient.Client.Transport = &hostSwitchingTransport{
   209  		primaryHost: apiHost,
   210  		primary:     utils.NewHttpTLSTransport(tlsConfig),
   211  		fallback:    http.DefaultTransport,
   212  	}
   213  
   214  	st := &state{
   215  		client: client,
   216  		conn:   conn,
   217  		clock:  clock,
   218  		addr:   apiHost,
   219  		cookieURL: &url.URL{
   220  			Scheme: "https",
   221  			Host:   conn.Config().Location.Host,
   222  			Path:   "/",
   223  		},
   224  		pingerFacadeVersion: facadeVersions["Pinger"],
   225  		serverScheme:        "https",
   226  		serverRootAddress:   conn.Config().Location.Host,
   227  		// We populate the username and password before
   228  		// login because, when doing HTTP requests, we'll want
   229  		// to use the same username and password for authenticating
   230  		// those. If login fails, we discard the connection.
   231  		tag:          tagToString(info.Tag),
   232  		password:     info.Password,
   233  		macaroons:    info.Macaroons,
   234  		nonce:        info.Nonce,
   235  		tlsConfig:    tlsConfig,
   236  		bakeryClient: bakeryClient,
   237  		modelTag:     info.ModelTag,
   238  	}
   239  	if !info.SkipLogin {
   240  		if err := st.Login(info.Tag, info.Password, info.Nonce, info.Macaroons); err != nil {
   241  			conn.Close()
   242  			return nil, errors.Trace(err)
   243  		}
   244  	}
   245  	st.broken = make(chan struct{})
   246  	st.closed = make(chan struct{})
   247  	go st.heartbeatMonitor()
   248  	return st, nil
   249  }
   250  
   251  // hostSwitchingTransport provides an http.RoundTripper
   252  // that chooses an actual RoundTripper to use
   253  // depending on the destination host.
   254  //
   255  // This makes it possible to use a different set of root
   256  // CAs for the API and all other hosts.
   257  type hostSwitchingTransport struct {
   258  	primaryHost string
   259  	primary     http.RoundTripper
   260  	fallback    http.RoundTripper
   261  }
   262  
   263  // RoundTrip implements http.RoundTripper.RoundTrip.
   264  func (t *hostSwitchingTransport) RoundTrip(req *http.Request) (*http.Response, error) {
   265  	if req.URL.Host == t.primaryHost {
   266  		return t.primary.RoundTrip(req)
   267  	}
   268  	return t.fallback.RoundTrip(req)
   269  }
   270  
   271  // connectWebsocket establishes a websocket connection to the RPC
   272  // API websocket on the API server using Info. If multiple API addresses
   273  // are provided in Info they will be tried concurrently - the first successful
   274  // connection wins.
   275  //
   276  // It also returns the TLS configuration that it has derived from the Info.
   277  func connectWebsocket(info *Info, opts DialOpts) (*websocket.Conn, *tls.Config, error) {
   278  	if len(info.Addrs) == 0 {
   279  		return nil, nil, errors.New("no API addresses to connect to")
   280  	}
   281  	tlsConfig := utils.SecureTLSConfig()
   282  	tlsConfig.InsecureSkipVerify = opts.InsecureSkipVerify
   283  
   284  	if info.CACert != "" && !tlsConfig.InsecureSkipVerify {
   285  		// We want to be specific here (rather than just using "anything".
   286  		// See commit 7fc118f015d8480dfad7831788e4b8c0432205e8 (PR 899).
   287  		tlsConfig.ServerName = "juju-apiserver"
   288  		certPool, err := CreateCertPool(info.CACert)
   289  		if err != nil {
   290  			return nil, nil, errors.Annotate(err, "cert pool creation failed")
   291  		}
   292  		tlsConfig.RootCAs = certPool
   293  	}
   294  	path, err := apiPath(info.ModelTag, "/api")
   295  	if err != nil {
   296  		return nil, nil, errors.Trace(err)
   297  	}
   298  	conn, err := dialWebSocket(info.Addrs, path, tlsConfig, opts)
   299  	if err != nil {
   300  		return nil, nil, errors.Trace(err)
   301  	}
   302  	logger.Infof("connection established to %q", conn.RemoteAddr())
   303  	return conn, tlsConfig, nil
   304  }
   305  
   306  // dialWebSocket dials a websocket with one of the provided addresses, the
   307  // specified URL path, TLS configuration, and dial options. Each of the
   308  // specified addresses will be attempted concurrently, and the first
   309  // successful connection will be returned.
   310  func dialWebSocket(addrs []string, path string, tlsConfig *tls.Config, opts DialOpts) (*websocket.Conn, error) {
   311  	// Dial all addresses at reasonable intervals.
   312  	try := parallel.NewTry(0, nil)
   313  	defer try.Kill()
   314  	for _, addr := range addrs {
   315  		err := dialWebsocket(addr, path, opts, tlsConfig, try)
   316  		if err == parallel.ErrStopped {
   317  			break
   318  		}
   319  		if err != nil {
   320  			return nil, errors.Trace(err)
   321  		}
   322  		select {
   323  		case <-time.After(opts.DialAddressInterval):
   324  		case <-try.Dead():
   325  		}
   326  	}
   327  	try.Close()
   328  	result, err := try.Result()
   329  	if err != nil {
   330  		return nil, errors.Trace(err)
   331  	}
   332  	return result.(*websocket.Conn), nil
   333  }
   334  
   335  // ConnectStream implements StreamConnector.ConnectStream.
   336  func (st *state) ConnectStream(path string, attrs url.Values) (base.Stream, error) {
   337  	if !st.isLoggedIn() {
   338  		return nil, errors.New("cannot use ConnectStream without logging in")
   339  	}
   340  	// We use the standard "macaraq" macaroon authentication dance here.
   341  	// That is, we attach any macaroons we have to the initial request,
   342  	// and if that succeeds, all's good. If it fails with a DischargeRequired
   343  	// error, the response will contain a macaroon that, when discharged,
   344  	// may allow access, so we discharge it (using bakery.Client.HandleError)
   345  	// and try the request again.
   346  	conn, err := st.connectStream(path, attrs)
   347  	if err == nil {
   348  		return conn, err
   349  	}
   350  	if params.ErrCode(err) != params.CodeDischargeRequired {
   351  		return nil, errors.Trace(err)
   352  	}
   353  	if err := st.bakeryClient.HandleError(st.cookieURL, bakeryError(err)); err != nil {
   354  		return nil, errors.Trace(err)
   355  	}
   356  	// Try again with the discharged macaroon.
   357  	conn, err = st.connectStream(path, attrs)
   358  	if err != nil {
   359  		return nil, errors.Trace(err)
   360  	}
   361  	return conn, nil
   362  }
   363  
   364  // connectStream is the internal version of ConnectStream. It differs from
   365  // ConnectStream only in that it will not retry the connection if it encounters
   366  // discharge-required error.
   367  func (st *state) connectStream(path string, attrs url.Values) (base.Stream, error) {
   368  	path, err := apiPath(st.modelTag, path)
   369  	if err != nil {
   370  		return nil, errors.Trace(err)
   371  	}
   372  	target := url.URL{
   373  		Scheme:   "wss",
   374  		Host:     st.addr,
   375  		Path:     path,
   376  		RawQuery: attrs.Encode(),
   377  	}
   378  	cfg, err := websocket.NewConfig(target.String(), "http://localhost/")
   379  	if st.tag != "" {
   380  		cfg.Header = utils.BasicAuthHeader(st.tag, st.password)
   381  	}
   382  	if st.nonce != "" {
   383  		cfg.Header.Set(params.MachineNonceHeader, st.nonce)
   384  	}
   385  	// Add any cookies because they will not be sent to websocket
   386  	// connections by default.
   387  	st.addCookiesToHeader(cfg.Header)
   388  
   389  	cfg.TlsConfig = st.tlsConfig
   390  	connection, err := websocketDialConfig(cfg)
   391  	if err != nil {
   392  		return nil, err
   393  	}
   394  	if err := readInitialStreamError(connection); err != nil {
   395  		return nil, errors.Trace(err)
   396  	}
   397  	return connection, nil
   398  }
   399  
   400  // readInitialStreamError reads the initial error response
   401  // from a stream connection and returns it.
   402  func readInitialStreamError(conn io.Reader) error {
   403  	// We can use bufio here because the websocket guarantees that a
   404  	// single read will not read more than a single frame; there is
   405  	// no guarantee that a single read might not read less than the
   406  	// whole frame though, so using a single Read call is not
   407  	// correct. By using ReadSlice rather than ReadBytes, we
   408  	// guarantee that the error can't be too big (>4096 bytes).
   409  	line, err := bufio.NewReader(conn).ReadSlice('\n')
   410  	if err != nil {
   411  		return errors.Annotate(err, "unable to read initial response")
   412  	}
   413  	var errResult params.ErrorResult
   414  	if err := json.Unmarshal(line, &errResult); err != nil {
   415  		return errors.Annotate(err, "unable to unmarshal initial response")
   416  	}
   417  	if errResult.Error != nil {
   418  		return errResult.Error
   419  	}
   420  	return nil
   421  }
   422  
   423  // addCookiesToHeader adds any cookies associated with the
   424  // API host to the given header. This is necessary because
   425  // otherwise cookies are not sent to websocket endpoints.
   426  func (st *state) addCookiesToHeader(h http.Header) {
   427  	// net/http only allows adding cookies to a request,
   428  	// but when it sends a request to a non-http endpoint,
   429  	// it doesn't add the cookies, so make a request, starting
   430  	// with the given header, add the cookies to use, then
   431  	// throw away the request but keep the header.
   432  	req := &http.Request{
   433  		Header: h,
   434  	}
   435  	cookies := st.bakeryClient.Client.Jar.Cookies(st.cookieURL)
   436  	for _, c := range cookies {
   437  		req.AddCookie(c)
   438  	}
   439  }
   440  
   441  // apiEndpoint returns a URL that refers to the given API slash-prefixed
   442  // endpoint path and query parameters.
   443  func (st *state) apiEndpoint(path, query string) (*url.URL, error) {
   444  	path, err := apiPath(st.modelTag, path)
   445  	if err != nil {
   446  		return nil, errors.Trace(err)
   447  	}
   448  	return &url.URL{
   449  		Scheme:   st.serverScheme,
   450  		Host:     st.Addr(),
   451  		Path:     path,
   452  		RawQuery: query,
   453  	}, nil
   454  }
   455  
   456  // apiPath returns the given API endpoint path relative
   457  // to the given model tag.
   458  func apiPath(modelTag names.ModelTag, path string) (string, error) {
   459  	if !strings.HasPrefix(path, "/") {
   460  		return "", errors.Errorf("cannot make API path from non-slash-prefixed path %q", path)
   461  	}
   462  	modelUUID := modelTag.Id()
   463  	if modelUUID == "" {
   464  		return path, nil
   465  	}
   466  	return "/model/" + modelUUID + path, nil
   467  }
   468  
   469  // tagToString returns the value of a tag's String method, or "" if the tag is nil.
   470  func tagToString(tag names.Tag) string {
   471  	if tag == nil {
   472  		return ""
   473  	}
   474  	return tag.String()
   475  }
   476  
   477  func dialWebsocket(addr, path string, opts DialOpts, tlsConfig *tls.Config, try *parallel.Try) error {
   478  	// origin is required by the WebSocket API, used for "origin policy"
   479  	// in websockets. We pass localhost to satisfy the API; it is
   480  	// inconsequential to us.
   481  	const origin = "http://localhost/"
   482  	cfg, err := websocket.NewConfig("wss://"+addr+path, origin)
   483  	if err != nil {
   484  		return errors.Trace(err)
   485  	}
   486  	cfg.TlsConfig = tlsConfig
   487  	return try.Start(newWebsocketDialer(cfg, opts))
   488  }
   489  
   490  // newWebsocketDialer returns a function that
   491  // can be passed to utils/parallel.Try.Start.
   492  var newWebsocketDialer = createWebsocketDialer
   493  
   494  func createWebsocketDialer(cfg *websocket.Config, opts DialOpts) func(<-chan struct{}) (io.Closer, error) {
   495  	// TODO(katco): 2016-08-09: lp:1611427
   496  	openAttempt := utils.AttemptStrategy{
   497  		Total: opts.Timeout,
   498  		Delay: opts.RetryDelay,
   499  	}
   500  	return func(stop <-chan struct{}) (io.Closer, error) {
   501  		for a := openAttempt.Start(); a.Next(); {
   502  			select {
   503  			case <-stop:
   504  				return nil, parallel.ErrStopped
   505  			default:
   506  			}
   507  			logger.Infof("dialing %q", cfg.Location)
   508  			conn, err := websocket.DialConfig(cfg)
   509  			if err == nil {
   510  				return conn, nil
   511  			}
   512  			if !a.HasNext() || isX509Error(err) {
   513  				// We won't reconnect when there's an X509 error
   514  				// because we're not going to succeed if we retry
   515  				// in that case.
   516  				logger.Infof("error dialing %q: %v", cfg.Location, err)
   517  				return nil, errors.Annotatef(err, "unable to connect to API")
   518  			}
   519  		}
   520  		panic("unreachable")
   521  	}
   522  }
   523  
   524  // isX509Error reports whether the given websocket error
   525  // results from an X509 problem.
   526  func isX509Error(err error) bool {
   527  	wsErr, ok := errors.Cause(err).(*websocket.DialError)
   528  	if !ok {
   529  		return false
   530  	}
   531  	switch wsErr.Err.(type) {
   532  	case x509.HostnameError,
   533  		x509.InsecureAlgorithmError,
   534  		x509.UnhandledCriticalExtension,
   535  		x509.UnknownAuthorityError,
   536  		x509.ConstraintViolationError,
   537  		x509.SystemRootsError:
   538  		return true
   539  	}
   540  	switch err {
   541  	case x509.ErrUnsupportedAlgorithm,
   542  		x509.IncorrectPasswordError:
   543  		return true
   544  	}
   545  	return false
   546  }
   547  
   548  func callWithTimeout(f func() error, timeout time.Duration) bool {
   549  	result := make(chan error, 1)
   550  	go func() {
   551  		// Note that result is buffered so that we don't leak this
   552  		// goroutine when a timeout happens.
   553  		result <- f()
   554  	}()
   555  	select {
   556  	case err := <-result:
   557  		if err != nil {
   558  			logger.Debugf("health ping failed: %v", err)
   559  		}
   560  		return err == nil
   561  	case <-time.After(timeout):
   562  		logger.Errorf("health ping timed out after %s", timeout)
   563  		return false
   564  	}
   565  }
   566  
   567  func (s *state) heartbeatMonitor() {
   568  	for {
   569  		if !callWithTimeout(s.Ping, PingTimeout) {
   570  			close(s.broken)
   571  			return
   572  		}
   573  		select {
   574  		case <-time.After(PingPeriod):
   575  		case <-s.closed:
   576  		}
   577  	}
   578  }
   579  
   580  func (s *state) Ping() error {
   581  	return s.APICall("Pinger", s.pingerFacadeVersion, "", "Ping", nil, nil)
   582  }
   583  
   584  type hasErrorCode interface {
   585  	ErrorCode() string
   586  }
   587  
   588  // APICall places a call to the remote machine.
   589  //
   590  // This fills out the rpc.Request on the given facade, version for a given
   591  // object id, and the specific RPC method. It marshalls the Arguments, and will
   592  // unmarshall the result into the response object that is supplied.
   593  func (s *state) APICall(facade string, version int, id, method string, args, response interface{}) error {
   594  	retrySpec := retry.CallArgs{
   595  		Func: func() error {
   596  			return s.client.Call(rpc.Request{
   597  				Type:    facade,
   598  				Version: version,
   599  				Id:      id,
   600  				Action:  method,
   601  			}, args, response)
   602  		},
   603  		IsFatalError: func(err error) bool {
   604  			err = errors.Cause(err)
   605  			ec, ok := err.(hasErrorCode)
   606  			if !ok {
   607  				return true
   608  			}
   609  			return ec.ErrorCode() != params.CodeRetry
   610  		},
   611  		Delay:       100 * time.Millisecond,
   612  		MaxDelay:    1500 * time.Millisecond,
   613  		MaxDuration: 10 * time.Second,
   614  		BackoffFunc: retry.DoubleDelay,
   615  		Clock:       s.clock,
   616  	}
   617  	err := retry.Call(retrySpec)
   618  	return errors.Trace(err)
   619  }
   620  
   621  func (s *state) Close() error {
   622  	err := s.client.Close()
   623  	select {
   624  	case <-s.closed:
   625  	default:
   626  		close(s.closed)
   627  	}
   628  	<-s.broken
   629  	return err
   630  }
   631  
   632  // Broken returns a channel that's closed when the connection is broken.
   633  func (s *state) Broken() <-chan struct{} {
   634  	return s.broken
   635  }
   636  
   637  // Addr returns the address used to connect to the API server.
   638  func (s *state) Addr() string {
   639  	return s.addr
   640  }
   641  
   642  // ModelTag implements base.APICaller.ModelTag.
   643  func (s *state) ModelTag() (names.ModelTag, bool) {
   644  	return s.modelTag, s.modelTag.Id() != ""
   645  }
   646  
   647  // ControllerTag implements base.APICaller.ControllerTag.
   648  func (s *state) ControllerTag() names.ControllerTag {
   649  	return s.controllerTag
   650  }
   651  
   652  // APIHostPorts returns addresses that may be used to connect
   653  // to the API server, including the address used to connect.
   654  //
   655  // The addresses are scoped (public, cloud-internal, etc.), so
   656  // the client may choose which addresses to attempt. For the
   657  // Juju CLI, all addresses must be attempted, as the CLI may
   658  // be invoked both within and outside the model (think
   659  // private clouds).
   660  func (s *state) APIHostPorts() [][]network.HostPort {
   661  	// NOTE: We're making a copy of s.hostPorts before returning it,
   662  	// for safety.
   663  	hostPorts := make([][]network.HostPort, len(s.hostPorts))
   664  	for i, server := range s.hostPorts {
   665  		hostPorts[i] = append([]network.HostPort{}, server...)
   666  	}
   667  	return hostPorts
   668  }
   669  
   670  // AllFacadeVersions returns what versions we know about for all facades
   671  func (s *state) AllFacadeVersions() map[string][]int {
   672  	facades := make(map[string][]int, len(s.facadeVersions))
   673  	for name, versions := range s.facadeVersions {
   674  		facades[name] = append([]int{}, versions...)
   675  	}
   676  	return facades
   677  }
   678  
   679  // BestFacadeVersion compares the versions of facades that we know about, and
   680  // the versions available from the server, and reports back what version is the
   681  // 'best available' to use.
   682  // TODO(jam) this is the eventual implementation of what version of a given
   683  // Facade we will want to use. It needs to line up the versions that the server
   684  // reports to us, with the versions that our client knows how to use.
   685  func (s *state) BestFacadeVersion(facade string) int {
   686  	return bestVersion(facadeVersions[facade], s.facadeVersions[facade])
   687  }
   688  
   689  // serverRoot returns the cached API server address and port used
   690  // to login, prefixed with "<URI scheme>://" (usually https).
   691  func (s *state) serverRoot() string {
   692  	return s.serverScheme + "://" + s.serverRootAddress
   693  }
   694  
   695  func (s *state) isLoggedIn() bool {
   696  	return atomic.LoadInt32(&s.loggedIn) == 1
   697  }
   698  
   699  func (s *state) setLoggedIn() {
   700  	atomic.StoreInt32(&s.loggedIn, 1)
   701  }