github.com/Pankov404/juju@v0.0.0-20150703034450-be266991dceb/api/apiclient.go (about)

     1  // Copyright 2012, 2013 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package api
     5  
     6  import (
     7  	"crypto/tls"
     8  	"crypto/x509"
     9  	"io"
    10  	"net/http"
    11  	"strings"
    12  	"time"
    13  
    14  	"github.com/juju/errors"
    15  	"github.com/juju/loggo"
    16  	"github.com/juju/names"
    17  	"github.com/juju/utils"
    18  	"github.com/juju/utils/parallel"
    19  	"golang.org/x/net/websocket"
    20  
    21  	"github.com/juju/juju/apiserver/params"
    22  	"github.com/juju/juju/cert"
    23  	"github.com/juju/juju/network"
    24  	"github.com/juju/juju/rpc"
    25  	"github.com/juju/juju/rpc/jsoncodec"
    26  	"github.com/juju/juju/version"
    27  )
    28  
    29  var logger = loggo.GetLogger("juju.api")
    30  
    31  // PingPeriod defines how often the internal connection health check
    32  // will run. It's a variable so it can be changed in tests.
    33  var PingPeriod = 1 * time.Minute
    34  
    35  type State struct {
    36  	client *rpc.Conn
    37  	conn   *websocket.Conn
    38  
    39  	// addr is the address used to connect to the API server.
    40  	addr string
    41  
    42  	// environTag holds the environment tag once we're connected
    43  	environTag string
    44  
    45  	// serverTag holds the server tag once we're connected.
    46  	// This is only set with newer apiservers where they are using
    47  	// the v1 login mechansim.
    48  	serverTag string
    49  
    50  	// serverVersion holds the version of the API server that we are
    51  	// connected to.  It is possible that this version is 0 if the
    52  	// server does not report this during login.
    53  	serverVersion version.Number
    54  
    55  	// hostPorts is the API server addresses returned from Login,
    56  	// which the client may cache and use for failover.
    57  	hostPorts [][]network.HostPort
    58  
    59  	// facadeVersions holds the versions of all facades as reported by
    60  	// Login
    61  	facadeVersions map[string][]int
    62  
    63  	// authTag holds the authenticated entity's tag after login.
    64  	authTag names.Tag
    65  
    66  	// broken is a channel that gets closed when the connection is
    67  	// broken.
    68  	broken chan struct{}
    69  
    70  	// closed is a channel that gets closed when State.Close is called.
    71  	closed chan struct{}
    72  
    73  	// tag and password hold the cached login credentials.
    74  	tag      string
    75  	password string
    76  
    77  	// serverRootAddress holds the cached API server address and port used
    78  	// to login.
    79  	serverRootAddress string
    80  
    81  	// serverScheme is the URI scheme of the API Server
    82  	serverScheme string
    83  
    84  	// certPool holds the cert pool that is used to authenticate the tls
    85  	// connections to the API.
    86  	certPool *x509.CertPool
    87  }
    88  
    89  // Info encapsulates information about a server holding juju state and
    90  // can be used to make a connection to it.
    91  type Info struct {
    92  	// Addrs holds the addresses of the state servers.
    93  	Addrs []string
    94  
    95  	// CACert holds the CA certificate that will be used
    96  	// to validate the state server's certificate, in PEM format.
    97  	CACert string
    98  
    99  	// Tag holds the name of the entity that is connecting.
   100  	// If this is nil, and the password are empty, no login attempt will be made.
   101  	// (this is to allow tests to access the API to check that operations
   102  	// fail when not logged in).
   103  	Tag names.Tag
   104  
   105  	// Password holds the password for the administrator or connecting entity.
   106  	Password string
   107  
   108  	// Nonce holds the nonce used when provisioning the machine. Used
   109  	// only by the machine agent.
   110  	Nonce string `yaml:",omitempty"`
   111  
   112  	// EnvironTag holds the environ tag for the environment we are
   113  	// trying to connect to.
   114  	EnvironTag names.EnvironTag
   115  }
   116  
   117  // DialOpts holds configuration parameters that control the
   118  // Dialing behavior when connecting to a state server.
   119  type DialOpts struct {
   120  	// DialAddressInterval is the amount of time to wait
   121  	// before starting to dial another address.
   122  	DialAddressInterval time.Duration
   123  
   124  	// Timeout is the amount of time to wait contacting
   125  	// a state server.
   126  	Timeout time.Duration
   127  
   128  	// RetryDelay is the amount of time to wait between
   129  	// unsucssful connection attempts.
   130  	RetryDelay time.Duration
   131  }
   132  
   133  // DefaultDialOpts returns a DialOpts representing the default
   134  // parameters for contacting a state server.
   135  func DefaultDialOpts() DialOpts {
   136  	return DialOpts{
   137  		DialAddressInterval: 50 * time.Millisecond,
   138  		Timeout:             10 * time.Minute,
   139  		RetryDelay:          2 * time.Second,
   140  	}
   141  }
   142  
   143  // Open establishes a connection to the API server using the Info
   144  // given, returning a State instance which can be used to make API
   145  // requests.
   146  //
   147  // See Connect for details of the connection mechanics.
   148  func Open(info *Info, opts DialOpts) (*State, error) {
   149  	return open(info, opts, (*State).Login)
   150  }
   151  
   152  // This unexported open method is used both directly above in the Open
   153  // function, and also the OpenWithVersion function below to explicitly cause
   154  // the API server to think that the client is older than it really is.
   155  func open(info *Info, opts DialOpts, loginFunc func(st *State, tag, pwd, nonce string) error) (*State, error) {
   156  	conn, err := Connect(info, "", nil, opts)
   157  	if err != nil {
   158  		return nil, errors.Trace(err)
   159  	}
   160  
   161  	client := rpc.NewConn(jsoncodec.NewWebsocket(conn), nil)
   162  	client.Start()
   163  	st := &State{
   164  		client:            client,
   165  		conn:              conn,
   166  		addr:              conn.Config().Location.Host,
   167  		serverScheme:      "https",
   168  		serverRootAddress: conn.Config().Location.Host,
   169  		// why are the contents of the tag (username and password) written into the
   170  		// state structure BEFORE login ?!?
   171  		tag:      toString(info.Tag),
   172  		password: info.Password,
   173  		certPool: conn.Config().TlsConfig.RootCAs,
   174  	}
   175  	if info.Tag != nil || info.Password != "" {
   176  		if err := loginFunc(st, info.Tag.String(), info.Password, info.Nonce); err != nil {
   177  			conn.Close()
   178  			return nil, err
   179  		}
   180  	}
   181  	st.broken = make(chan struct{})
   182  	st.closed = make(chan struct{})
   183  	go st.heartbeatMonitor()
   184  	return st, nil
   185  }
   186  
   187  // OpenWithVersion uses an explicit version of the Admin facade to call Login
   188  // on. This allows the caller to pretend to be an older client, and is used
   189  // only in testing.
   190  func OpenWithVersion(info *Info, opts DialOpts, loginVersion int) (*State, error) {
   191  	var loginFunc func(st *State, tag, pwd, nonce string) error
   192  	switch loginVersion {
   193  	case 0:
   194  		loginFunc = (*State).loginV0
   195  	case 1:
   196  		loginFunc = (*State).loginV1
   197  	case 2:
   198  		loginFunc = (*State).loginV2
   199  	default:
   200  		return nil, errors.NotSupportedf("loginVersion %d", loginVersion)
   201  	}
   202  	return open(info, opts, loginFunc)
   203  }
   204  
   205  // Connect establishes a websocket connection to the API server using
   206  // the Info, API path tail and (optional) request headers provided. If
   207  // multiple API addresses are provided in Info they will be tried
   208  // concurrently - the first successful connection wins.
   209  //
   210  // The path tail may be blank, in which case the default value will be
   211  // used. Otherwise, it must start with a "/".
   212  func Connect(info *Info, pathTail string, header http.Header, opts DialOpts) (*websocket.Conn, error) {
   213  	if len(info.Addrs) == 0 {
   214  		return nil, errors.New("no API addresses to connect to")
   215  	}
   216  	if pathTail != "" && !strings.HasPrefix(pathTail, "/") {
   217  		return nil, errors.New(`path tail must start with "/"`)
   218  	}
   219  
   220  	pool := x509.NewCertPool()
   221  	xcert, err := cert.ParseCert(info.CACert)
   222  	if err != nil {
   223  		return nil, errors.Annotate(err, "cert pool creation failed")
   224  	}
   225  	pool.AddCert(xcert)
   226  
   227  	path := makeAPIPath(info.EnvironTag.Id(), pathTail)
   228  
   229  	// Dial all addresses at reasonable intervals.
   230  	try := parallel.NewTry(0, nil)
   231  	defer try.Kill()
   232  	for _, addr := range info.Addrs {
   233  		err := dialWebsocket(addr, path, header, opts, pool, try)
   234  		if err == parallel.ErrStopped {
   235  			break
   236  		}
   237  		if err != nil {
   238  			return nil, errors.Trace(err)
   239  		}
   240  		select {
   241  		case <-time.After(opts.DialAddressInterval):
   242  		case <-try.Dead():
   243  		}
   244  	}
   245  	try.Close()
   246  	result, err := try.Result()
   247  	if err != nil {
   248  		return nil, errors.Trace(err)
   249  	}
   250  	conn := result.(*websocket.Conn)
   251  	logger.Infof("connection established to %q", conn.RemoteAddr())
   252  	return conn, nil
   253  }
   254  
   255  // makeAPIPath builds the path to connect to based on the tail given
   256  // and whether the environment UUID is set.
   257  func makeAPIPath(envUUID, tail string) string {
   258  	if envUUID == "" {
   259  		if tail == "" {
   260  			tail = "/"
   261  		}
   262  		return tail
   263  	}
   264  	if tail == "" {
   265  		tail = "/api"
   266  	}
   267  	return "/environment/" + envUUID + tail
   268  }
   269  
   270  // toString returns the value of a tag's String method, or "" if the tag is nil.
   271  func toString(tag names.Tag) string {
   272  	if tag == nil {
   273  		return ""
   274  	}
   275  	return tag.String()
   276  }
   277  
   278  func dialWebsocket(addr, path string, header http.Header, opts DialOpts, rootCAs *x509.CertPool, try *parallel.Try) error {
   279  	cfg, err := setUpWebsocket(addr, path, header, rootCAs)
   280  	if err != nil {
   281  		return err
   282  	}
   283  	return try.Start(newWebsocketDialer(cfg, opts))
   284  }
   285  
   286  func setUpWebsocket(addr, path string, header http.Header, rootCAs *x509.CertPool) (*websocket.Config, error) {
   287  	// origin is required by the WebSocket API, used for "origin policy"
   288  	// in websockets. We pass localhost to satisfy the API; it is
   289  	// inconsequential to us.
   290  	const origin = "http://localhost/"
   291  	cfg, err := websocket.NewConfig("wss://"+addr+path, origin)
   292  	if err != nil {
   293  		return nil, errors.Trace(err)
   294  	}
   295  	cfg.TlsConfig = &tls.Config{
   296  		RootCAs:    rootCAs,
   297  		ServerName: "juju-apiserver",
   298  	}
   299  	cfg.Header = header
   300  	return cfg, nil
   301  }
   302  
   303  // newWebsocketDialer returns a function that
   304  // can be passed to utils/parallel.Try.Start.
   305  var newWebsocketDialer = createWebsocketDialer
   306  
   307  func createWebsocketDialer(cfg *websocket.Config, opts DialOpts) func(<-chan struct{}) (io.Closer, error) {
   308  	openAttempt := utils.AttemptStrategy{
   309  		Total: opts.Timeout,
   310  		Delay: opts.RetryDelay,
   311  	}
   312  	return func(stop <-chan struct{}) (io.Closer, error) {
   313  		for a := openAttempt.Start(); a.Next(); {
   314  			select {
   315  			case <-stop:
   316  				return nil, parallel.ErrStopped
   317  			default:
   318  			}
   319  			logger.Infof("dialing %q", cfg.Location)
   320  			conn, err := websocket.DialConfig(cfg)
   321  			if err == nil {
   322  				return conn, nil
   323  			}
   324  			if a.HasNext() {
   325  				logger.Debugf("error dialing %q, will retry: %v", cfg.Location, err)
   326  			} else {
   327  				logger.Infof("error dialing %q: %v", cfg.Location, err)
   328  				return nil, errors.Errorf("unable to connect to %q", cfg.Location)
   329  			}
   330  		}
   331  		panic("unreachable")
   332  	}
   333  }
   334  
   335  func (s *State) heartbeatMonitor() {
   336  	for {
   337  		if err := s.Ping(); err != nil {
   338  			close(s.broken)
   339  			return
   340  		}
   341  		select {
   342  		case <-time.After(PingPeriod):
   343  		case <-s.closed:
   344  		}
   345  	}
   346  }
   347  
   348  func (s *State) Ping() error {
   349  	return s.APICall("Pinger", s.BestFacadeVersion("Pinger"), "", "Ping", nil, nil)
   350  }
   351  
   352  // APICall places a call to the remote machine.
   353  //
   354  // This fills out the rpc.Request on the given facade, version for a given
   355  // object id, and the specific RPC method. It marshalls the Arguments, and will
   356  // unmarshall the result into the response object that is supplied.
   357  func (s *State) APICall(facade string, version int, id, method string, args, response interface{}) error {
   358  	err := s.client.Call(rpc.Request{
   359  		Type:    facade,
   360  		Version: version,
   361  		Id:      id,
   362  		Action:  method,
   363  	}, args, response)
   364  	return params.ClientError(err)
   365  }
   366  
   367  func (s *State) Close() error {
   368  	err := s.client.Close()
   369  	select {
   370  	case <-s.closed:
   371  	default:
   372  		close(s.closed)
   373  	}
   374  	<-s.broken
   375  	return err
   376  }
   377  
   378  // Broken returns a channel that's closed when the connection is broken.
   379  func (s *State) Broken() <-chan struct{} {
   380  	return s.broken
   381  }
   382  
   383  // RPCClient returns the RPC client for the state, so that testing
   384  // functions can tickle parts of the API that the conventional entry
   385  // points don't reach. This is exported for testing purposes only.
   386  func (s *State) RPCClient() *rpc.Conn {
   387  	return s.client
   388  }
   389  
   390  // Addr returns the address used to connect to the API server.
   391  func (s *State) Addr() string {
   392  	return s.addr
   393  }
   394  
   395  // EnvironTag returns the tag of the environment we are connected to.
   396  func (s *State) EnvironTag() (names.EnvironTag, error) {
   397  	return names.ParseEnvironTag(s.environTag)
   398  }
   399  
   400  // ServerTag returns the tag of the server we are connected to.
   401  func (s *State) ServerTag() (names.EnvironTag, error) {
   402  	return names.ParseEnvironTag(s.serverTag)
   403  }
   404  
   405  // APIHostPorts returns addresses that may be used to connect
   406  // to the API server, including the address used to connect.
   407  //
   408  // The addresses are scoped (public, cloud-internal, etc.), so
   409  // the client may choose which addresses to attempt. For the
   410  // Juju CLI, all addresses must be attempted, as the CLI may
   411  // be invoked both within and outside the environment (think
   412  // private clouds).
   413  func (s *State) APIHostPorts() [][]network.HostPort {
   414  	// NOTE: We're making a copy of s.hostPorts before returning it,
   415  	// for safety.
   416  	hostPorts := make([][]network.HostPort, len(s.hostPorts))
   417  	for i, server := range s.hostPorts {
   418  		hostPorts[i] = append([]network.HostPort{}, server...)
   419  	}
   420  	return hostPorts
   421  }
   422  
   423  // AllFacadeVersions returns what versions we know about for all facades
   424  func (s *State) AllFacadeVersions() map[string][]int {
   425  	facades := make(map[string][]int, len(s.facadeVersions))
   426  	for name, versions := range s.facadeVersions {
   427  		facades[name] = append([]int{}, versions...)
   428  	}
   429  	return facades
   430  }
   431  
   432  // BestFacadeVersion compares the versions of facades that we know about, and
   433  // the versions available from the server, and reports back what version is the
   434  // 'best available' to use.
   435  // TODO(jam) this is the eventual implementation of what version of a given
   436  // Facade we will want to use. It needs to line up the versions that the server
   437  // reports to us, with the versions that our client knows how to use.
   438  func (s *State) BestFacadeVersion(facade string) int {
   439  	return bestVersion(facadeVersions[facade], s.facadeVersions[facade])
   440  }
   441  
   442  // serverRoot returns the cached API server address and port used
   443  // to login, prefixed with "<URI scheme>://" (usually https).
   444  func (s *State) serverRoot() string {
   445  	return s.serverScheme + "://" + s.serverRootAddress
   446  }