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