launchpad.net/~rogpeppe/juju-core/500-errgo-fix@v0.0.0-20140213181702-000000002356/state/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  	"time"
    10  
    11  	"code.google.com/p/go.net/websocket"
    12  
    13  	"launchpad.net/errgo/errors"
    14  	"launchpad.net/juju-core/cert"
    15  	"launchpad.net/juju-core/log"
    16  	"launchpad.net/juju-core/rpc"
    17  	"launchpad.net/juju-core/rpc/jsoncodec"
    18  	"launchpad.net/juju-core/state/api/params"
    19  	"launchpad.net/juju-core/utils"
    20  )
    21  
    22  var mask = errors.Mask
    23  
    24  // PingPeriod defines how often the internal connection health check
    25  // will run. It's a variable so it can be changed in tests.
    26  var PingPeriod = 1 * time.Minute
    27  
    28  type State struct {
    29  	client *rpc.Conn
    30  	conn   *websocket.Conn
    31  
    32  	// authTag holds the authenticated entity's tag after login.
    33  	authTag string
    34  
    35  	// broken is a channel that gets closed when the connection is
    36  	// broken.
    37  	broken chan struct{}
    38  
    39  	// tag and password hold the cached login credentials.
    40  	tag      string
    41  	password string
    42  	// serverRoot holds the cached API server address and port we used
    43  	// to login, with a https:// prefix.
    44  	serverRoot string
    45  }
    46  
    47  // Info encapsulates information about a server holding juju state and
    48  // can be used to make a connection to it.
    49  type Info struct {
    50  	// Addrs holds the addresses of the state servers.
    51  	Addrs []string
    52  
    53  	// CACert holds the CA certificate that will be used
    54  	// to validate the state server's certificate, in PEM format.
    55  	CACert []byte
    56  
    57  	// Tag holds the name of the entity that is connecting.
    58  	// If this and the password are empty, no login attempt will be made
    59  	// (this is to allow tests to access the API to check that operations
    60  	// fail when not logged in).
    61  	Tag string
    62  
    63  	// Password holds the password for the administrator or connecting entity.
    64  	Password string
    65  
    66  	// Nonce holds the nonce used when provisioning the machine. Used
    67  	// only by the machine agent.
    68  	Nonce string `yaml:",omitempty"`
    69  }
    70  
    71  var openAttempt = utils.AttemptStrategy{
    72  	Total: 5 * time.Minute,
    73  	Delay: 500 * time.Millisecond,
    74  }
    75  
    76  // DialOpts holds configuration parameters that control the
    77  // Dialing behavior when connecting to a state server.
    78  type DialOpts struct {
    79  	// Timeout is the amount of time to wait contacting
    80  	// a state server.
    81  	Timeout time.Duration
    82  
    83  	// RetryDelay is the amount of time to wait between
    84  	// unsucssful connection attempts.
    85  	RetryDelay time.Duration
    86  }
    87  
    88  // DefaultDialOpts returns a DialOpts representing the default
    89  // parameters for contacting a state server.
    90  func DefaultDialOpts() DialOpts {
    91  	return DialOpts{
    92  		Timeout:    10 * time.Minute,
    93  		RetryDelay: 2 * time.Second,
    94  	}
    95  }
    96  
    97  func Open(info *Info, opts DialOpts) (*State, error) {
    98  	// TODO Select a random address from info.Addrs
    99  	// and only fail when we've tried all the addresses.
   100  	// TODO what does "origin" really mean, and is localhost always ok?
   101  	cfg, err := websocket.NewConfig("wss://"+info.Addrs[0]+"/", "http://localhost/")
   102  	if err != nil {
   103  		return nil, mask(err)
   104  	}
   105  	pool := x509.NewCertPool()
   106  	xcert, err := cert.ParseCert(info.CACert)
   107  	if err != nil {
   108  		return nil, mask(err)
   109  	}
   110  	pool.AddCert(xcert)
   111  	cfg.TlsConfig = &tls.Config{
   112  		RootCAs:    pool,
   113  		ServerName: "anything",
   114  	}
   115  	var conn *websocket.Conn
   116  	openAttempt := utils.AttemptStrategy{
   117  		Total: opts.Timeout,
   118  		Delay: opts.RetryDelay,
   119  	}
   120  	for a := openAttempt.Start(); a.Next(); {
   121  		log.Infof("state/api: dialing %q", cfg.Location)
   122  		conn, err = websocket.DialConfig(cfg)
   123  		if err == nil {
   124  			break
   125  		}
   126  		log.Errorf("state/api: %v", err)
   127  	}
   128  	if err != nil {
   129  		return nil, mask(err)
   130  	}
   131  	log.Infof("state/api: connection established")
   132  
   133  	client := rpc.NewConn(jsoncodec.NewWebsocket(conn), nil)
   134  	client.Start()
   135  	st := &State{
   136  		client:     client,
   137  		conn:       conn,
   138  		serverRoot: "https://" + cfg.Location.Host,
   139  		tag:        info.Tag,
   140  		password:   info.Password,
   141  	}
   142  	if info.Tag != "" || info.Password != "" {
   143  		if err := st.Login(info.Tag, info.Password, info.Nonce); err != nil {
   144  			conn.Close()
   145  			return nil, err
   146  		}
   147  	}
   148  	st.broken = make(chan struct{})
   149  	go st.heartbeatMonitor()
   150  	return st, nil
   151  }
   152  
   153  func (s *State) heartbeatMonitor() {
   154  	for {
   155  		if err := s.Ping(); err != nil {
   156  			close(s.broken)
   157  			return
   158  		}
   159  		time.Sleep(PingPeriod)
   160  	}
   161  }
   162  
   163  func (s *State) Ping() error {
   164  	return s.Call("Pinger", "", "Ping", nil, nil)
   165  }
   166  
   167  // Call invokes a low-level RPC method of the given objType, id, and
   168  // request, passing the given parameters and filling in the response
   169  // results. This should not be used directly by clients.
   170  // TODO (dimitern) Add tests for all client-facing objects to verify
   171  // we return the correct error when invoking Call("Object",
   172  // "non-empty-id",...)
   173  func (s *State) Call(objType, id, request string, args, response interface{}) error {
   174  	err := s.client.Call(rpc.Request{
   175  		Type:   objType,
   176  		Id:     id,
   177  		Action: request,
   178  	}, args, response)
   179  	return params.ClientError(err)
   180  }
   181  
   182  func (s *State) Close() error {
   183  	return s.client.Close()
   184  }
   185  
   186  // Broken returns a channel that's closed when the connection is broken.
   187  func (s *State) Broken() <-chan struct{} {
   188  	return s.broken
   189  }
   190  
   191  // RPCClient returns the RPC client for the state, so that testing
   192  // functions can tickle parts of the API that the conventional entry
   193  // points don't reach. This is exported for testing purposes only.
   194  func (s *State) RPCClient() *rpc.Conn {
   195  	return s.client
   196  }