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