github.com/altoros/juju-vmware@v0.0.0-20150312064031-f19ae857ccca/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  	"fmt"
    10  	"io"
    11  	"strings"
    12  	"time"
    13  
    14  	"code.google.com/p/go.net/websocket"
    15  	"github.com/juju/loggo"
    16  	"github.com/juju/names"
    17  	"github.com/juju/utils"
    18  	"github.com/juju/utils/parallel"
    19  
    20  	"github.com/juju/juju/apiserver/params"
    21  	"github.com/juju/juju/cert"
    22  	"github.com/juju/juju/network"
    23  	"github.com/juju/juju/rpc"
    24  	"github.com/juju/juju/rpc/jsoncodec"
    25  )
    26  
    27  var logger = loggo.GetLogger("juju.api")
    28  
    29  // PingPeriod defines how often the internal connection health check
    30  // will run. It's a variable so it can be changed in tests.
    31  var PingPeriod = 1 * time.Minute
    32  
    33  type State struct {
    34  	client *rpc.Conn
    35  	conn   *websocket.Conn
    36  
    37  	// addr is the address used to connect to the API server.
    38  	addr string
    39  
    40  	// environTag holds the environment tag once we're connected
    41  	environTag string
    42  
    43  	// hostPorts is the API server addresses returned from Login,
    44  	// which the client may cache and use for failover.
    45  	hostPorts [][]network.HostPort
    46  
    47  	// facadeVersions holds the versions of all facades as reported by
    48  	// Login
    49  	facadeVersions map[string][]int
    50  
    51  	// authTag holds the authenticated entity's tag after login.
    52  	authTag names.Tag
    53  
    54  	// broken is a channel that gets closed when the connection is
    55  	// broken.
    56  	broken chan struct{}
    57  
    58  	// closed is a channel that gets closed when State.Close is called.
    59  	closed chan struct{}
    60  
    61  	// tag and password hold the cached login credentials.
    62  	tag      string
    63  	password string
    64  
    65  	// serverRoot holds the cached API server address and port we used
    66  	// to login, with a https:// prefix.
    67  	serverRoot string
    68  
    69  	// certPool holds the cert pool that is used to authenticate the tls
    70  	// connections to the API.
    71  	certPool *x509.CertPool
    72  }
    73  
    74  // Info encapsulates information about a server holding juju state and
    75  // can be used to make a connection to it.
    76  type Info struct {
    77  	// Addrs holds the addresses of the state servers.
    78  	Addrs []string
    79  
    80  	// CACert holds the CA certificate that will be used
    81  	// to validate the state server's certificate, in PEM format.
    82  	CACert string
    83  
    84  	// Tag holds the name of the entity that is connecting.
    85  	// If this is nil, and the password are empty, no login attempt will be made.
    86  	// (this is to allow tests to access the API to check that operations
    87  	// fail when not logged in).
    88  	Tag names.Tag
    89  
    90  	// Password holds the password for the administrator or connecting entity.
    91  	Password string
    92  
    93  	// Nonce holds the nonce used when provisioning the machine. Used
    94  	// only by the machine agent.
    95  	Nonce string `yaml:",omitempty"`
    96  
    97  	// EnvironTag holds the environ tag for the environment we are
    98  	// trying to connect to.
    99  	EnvironTag names.EnvironTag
   100  }
   101  
   102  // DialOpts holds configuration parameters that control the
   103  // Dialing behavior when connecting to a state server.
   104  type DialOpts struct {
   105  	// DialAddressInterval is the amount of time to wait
   106  	// before starting to dial another address.
   107  	DialAddressInterval time.Duration
   108  
   109  	// Timeout is the amount of time to wait contacting
   110  	// a state server.
   111  	Timeout time.Duration
   112  
   113  	// RetryDelay is the amount of time to wait between
   114  	// unsucssful connection attempts.
   115  	RetryDelay time.Duration
   116  }
   117  
   118  // DefaultDialOpts returns a DialOpts representing the default
   119  // parameters for contacting a state server.
   120  func DefaultDialOpts() DialOpts {
   121  	return DialOpts{
   122  		DialAddressInterval: 50 * time.Millisecond,
   123  		Timeout:             10 * time.Minute,
   124  		RetryDelay:          2 * time.Second,
   125  	}
   126  }
   127  
   128  func Open(info *Info, opts DialOpts) (*State, error) {
   129  	if len(info.Addrs) == 0 {
   130  		return nil, fmt.Errorf("no API addresses to connect to")
   131  	}
   132  	pool := x509.NewCertPool()
   133  	xcert, err := cert.ParseCert(info.CACert)
   134  	if err != nil {
   135  		return nil, err
   136  	}
   137  	pool.AddCert(xcert)
   138  
   139  	var environUUID string
   140  	if info.EnvironTag.Id() != "" {
   141  		environUUID = info.EnvironTag.Id()
   142  	}
   143  	// Dial all addresses at reasonable intervals.
   144  	try := parallel.NewTry(0, nil)
   145  	defer try.Kill()
   146  	var addrs []string
   147  	for _, addr := range info.Addrs {
   148  		if strings.HasPrefix(addr, "localhost:") {
   149  			addrs = append(addrs, addr)
   150  			break
   151  		}
   152  	}
   153  	if len(addrs) == 0 {
   154  		addrs = info.Addrs
   155  	}
   156  	for _, addr := range addrs {
   157  		err := dialWebsocket(addr, environUUID, opts, pool, try)
   158  		if err == parallel.ErrStopped {
   159  			break
   160  		}
   161  		if err != nil {
   162  			return nil, err
   163  		}
   164  		select {
   165  		case <-time.After(opts.DialAddressInterval):
   166  		case <-try.Dead():
   167  		}
   168  	}
   169  	try.Close()
   170  	result, err := try.Result()
   171  	if err != nil {
   172  		return nil, err
   173  	}
   174  	conn := result.(*websocket.Conn)
   175  	logger.Infof("connection established to %q", conn.RemoteAddr())
   176  
   177  	client := rpc.NewConn(jsoncodec.NewWebsocket(conn), nil)
   178  	client.Start()
   179  	st := &State{
   180  		client:     client,
   181  		conn:       conn,
   182  		addr:       conn.Config().Location.Host,
   183  		serverRoot: "https://" + conn.Config().Location.Host,
   184  		// why are the contents of the tag (username and password) written into the
   185  		// state structure BEFORE login ?!?
   186  		tag:      toString(info.Tag),
   187  		password: info.Password,
   188  		certPool: pool,
   189  	}
   190  	if info.Tag != nil || info.Password != "" {
   191  		if err := st.Login(info.Tag.String(), info.Password, info.Nonce); err != nil {
   192  			conn.Close()
   193  			return nil, err
   194  		}
   195  	}
   196  	st.broken = make(chan struct{})
   197  	st.closed = make(chan struct{})
   198  	go st.heartbeatMonitor()
   199  	return st, nil
   200  }
   201  
   202  // toString returns the value of a tag's String method, or "" if the tag is nil.
   203  func toString(tag names.Tag) string {
   204  	if tag == nil {
   205  		return ""
   206  	}
   207  	return tag.String()
   208  }
   209  
   210  func dialWebsocket(addr, environUUID string, opts DialOpts, rootCAs *x509.CertPool, try *parallel.Try) error {
   211  	cfg, err := setUpWebsocket(addr, environUUID, rootCAs)
   212  	if err != nil {
   213  		return err
   214  	}
   215  	return try.Start(newWebsocketDialer(cfg, opts))
   216  }
   217  
   218  func setUpWebsocket(addr, environUUID string, rootCAs *x509.CertPool) (*websocket.Config, error) {
   219  	// origin is required by the WebSocket API, used for "origin policy"
   220  	// in websockets. We pass localhost to satisfy the API; it is
   221  	// inconsequential to us.
   222  	const origin = "http://localhost/"
   223  	tail := "/"
   224  	if environUUID != "" {
   225  		tail = "/environment/" + environUUID + "/api"
   226  	}
   227  	cfg, err := websocket.NewConfig("wss://"+addr+tail, origin)
   228  	if err != nil {
   229  		return nil, err
   230  	}
   231  	cfg.TlsConfig = &tls.Config{
   232  		RootCAs:    rootCAs,
   233  		ServerName: "juju-apiserver",
   234  	}
   235  	return cfg, nil
   236  }
   237  
   238  // newWebsocketDialer returns a function that
   239  // can be passed to utils/parallel.Try.Start.
   240  func newWebsocketDialer(cfg *websocket.Config, opts DialOpts) func(<-chan struct{}) (io.Closer, error) {
   241  	openAttempt := utils.AttemptStrategy{
   242  		Total: opts.Timeout,
   243  		Delay: opts.RetryDelay,
   244  	}
   245  	return func(stop <-chan struct{}) (io.Closer, error) {
   246  		for a := openAttempt.Start(); a.Next(); {
   247  			select {
   248  			case <-stop:
   249  				return nil, parallel.ErrStopped
   250  			default:
   251  			}
   252  			logger.Infof("dialing %q", cfg.Location)
   253  			conn, err := websocket.DialConfig(cfg)
   254  			if err == nil {
   255  				return conn, nil
   256  			}
   257  			if a.HasNext() {
   258  				logger.Debugf("error dialing %q, will retry: %v", cfg.Location, err)
   259  			} else {
   260  				logger.Infof("error dialing %q: %v", cfg.Location, err)
   261  				return nil, fmt.Errorf("unable to connect to %q", cfg.Location)
   262  			}
   263  		}
   264  		panic("unreachable")
   265  	}
   266  }
   267  
   268  func (s *State) heartbeatMonitor() {
   269  	for {
   270  		if err := s.Ping(); err != nil {
   271  			close(s.broken)
   272  			return
   273  		}
   274  		select {
   275  		case <-time.After(PingPeriod):
   276  		case <-s.closed:
   277  		}
   278  	}
   279  }
   280  
   281  func (s *State) Ping() error {
   282  	return s.APICall("Pinger", s.BestFacadeVersion("Pinger"), "", "Ping", nil, nil)
   283  }
   284  
   285  // APICall places a call to the remote machine.
   286  //
   287  // This fills out the rpc.Request on the given facade, version for a given
   288  // object id, and the specific RPC method. It marshalls the Arguments, and will
   289  // unmarshall the result into the response object that is supplied.
   290  func (s *State) APICall(facade string, version int, id, method string, args, response interface{}) error {
   291  	err := s.client.Call(rpc.Request{
   292  		Type:    facade,
   293  		Version: version,
   294  		Id:      id,
   295  		Action:  method,
   296  	}, args, response)
   297  	return params.ClientError(err)
   298  }
   299  
   300  func (s *State) Close() error {
   301  	err := s.client.Close()
   302  	select {
   303  	case <-s.closed:
   304  	default:
   305  		close(s.closed)
   306  	}
   307  	<-s.broken
   308  	return err
   309  }
   310  
   311  // Broken returns a channel that's closed when the connection is broken.
   312  func (s *State) Broken() <-chan struct{} {
   313  	return s.broken
   314  }
   315  
   316  // RPCClient returns the RPC client for the state, so that testing
   317  // functions can tickle parts of the API that the conventional entry
   318  // points don't reach. This is exported for testing purposes only.
   319  func (s *State) RPCClient() *rpc.Conn {
   320  	return s.client
   321  }
   322  
   323  // Addr returns the address used to connect to the API server.
   324  func (s *State) Addr() string {
   325  	return s.addr
   326  }
   327  
   328  // EnvironTag returns the tag of the environment we are connected to.
   329  func (s *State) EnvironTag() (names.EnvironTag, error) {
   330  	return names.ParseEnvironTag(s.environTag)
   331  }
   332  
   333  // APIHostPorts returns addresses that may be used to connect
   334  // to the API server, including the address used to connect.
   335  //
   336  // The addresses are scoped (public, cloud-internal, etc.), so
   337  // the client may choose which addresses to attempt. For the
   338  // Juju CLI, all addresses must be attempted, as the CLI may
   339  // be invoked both within and outside the environment (think
   340  // private clouds).
   341  func (s *State) APIHostPorts() [][]network.HostPort {
   342  	hostPorts := make([][]network.HostPort, len(s.hostPorts))
   343  	for i, server := range s.hostPorts {
   344  		hostPorts[i] = append([]network.HostPort{}, server...)
   345  	}
   346  	return hostPorts
   347  }
   348  
   349  // AllFacadeVersions returns what versions we know about for all facades
   350  func (s *State) AllFacadeVersions() map[string][]int {
   351  	facades := make(map[string][]int, len(s.facadeVersions))
   352  	for name, versions := range s.facadeVersions {
   353  		facades[name] = append([]int{}, versions...)
   354  	}
   355  	return facades
   356  }
   357  
   358  // BestFacadeVersion compares the versions of facades that we know about, and
   359  // the versions available from the server, and reports back what version is the
   360  // 'best available' to use.
   361  // TODO(jam) this is the eventual implementation of what version of a given
   362  // Facade we will want to use. It needs to line up the versions that the server
   363  // reports to us, with the versions that our client knows how to use.
   364  func (s *State) BestFacadeVersion(facade string) int {
   365  	return bestVersion(facadeVersions[facade], s.facadeVersions[facade])
   366  }