github.com/makyo/juju@v0.0.0-20160425123129-2608902037e9/mongo/open.go (about)

     1  // Copyright 2014 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package mongo
     5  
     6  import (
     7  	"crypto/tls"
     8  	"crypto/x509"
     9  	stderrors "errors"
    10  	"fmt"
    11  	"net"
    12  	"time"
    13  
    14  	"github.com/juju/errors"
    15  	"github.com/juju/names"
    16  	"gopkg.in/mgo.v2"
    17  
    18  	"github.com/juju/juju/cert"
    19  )
    20  
    21  // SocketTimeout should be long enough that
    22  // even a slow mongo server will respond in that
    23  // length of time. Since mongo servers ping themselves
    24  // every 10 seconds, we use a value of just over 2
    25  // ping periods to allow for delayed pings due to
    26  // issues such as CPU starvation etc.
    27  const SocketTimeout = 21 * time.Second
    28  
    29  // defaultDialTimeout should be representative of
    30  // the upper bound of time taken to dial a mongo
    31  // server from within the same cloud/private network.
    32  const defaultDialTimeout = 30 * time.Second
    33  
    34  // DialOpts holds configuration parameters that control the
    35  // Dialing behavior when connecting to a controller.
    36  type DialOpts struct {
    37  	// Timeout is the amount of time to wait contacting
    38  	// a controller.
    39  	Timeout time.Duration
    40  
    41  	// SocketTimeout is the amount of time to wait for a
    42  	// non-responding socket to the database before it is forcefully
    43  	// closed. If this is zero, the value of the SocketTimeout const
    44  	// will be used.
    45  	SocketTimeout time.Duration
    46  
    47  	// Direct informs whether to establish connections only with the
    48  	// specified seed servers, or to obtain information for the whole
    49  	// cluster and establish connections with further servers too.
    50  	Direct bool
    51  
    52  	// PostDial, if non-nil, is called by DialWithInfo with the
    53  	// mgo.Session after a successful dial but before DialWithInfo
    54  	// returns to its caller.
    55  	PostDial func(*mgo.Session) error
    56  }
    57  
    58  // DefaultDialOpts returns a DialOpts representing the default
    59  // parameters for contacting a controller.
    60  func DefaultDialOpts() DialOpts {
    61  	return DialOpts{
    62  		Timeout:       defaultDialTimeout,
    63  		SocketTimeout: SocketTimeout,
    64  	}
    65  }
    66  
    67  // Info encapsulates information about cluster of
    68  // mongo servers and can be used to make a
    69  // connection to that cluster.
    70  type Info struct {
    71  	// Addrs gives the addresses of the MongoDB servers for the state.
    72  	// Each address should be in the form address:port.
    73  	Addrs []string
    74  
    75  	// CACert holds the CA certificate that will be used
    76  	// to validate the controller's certificate, in PEM format.
    77  	CACert string
    78  }
    79  
    80  // MongoInfo encapsulates information about cluster of
    81  // servers holding juju state and can be used to make a
    82  // connection to that cluster.
    83  type MongoInfo struct {
    84  	// mongo.Info contains the addresses and cert of the mongo cluster.
    85  	Info
    86  	// Tag holds the name of the entity that is connecting.
    87  	// It should be nil when connecting as an administrator.
    88  	Tag names.Tag
    89  
    90  	// Password holds the password for the connecting entity.
    91  	Password string
    92  }
    93  
    94  // DialInfo returns information on how to dial
    95  // the state's mongo server with the given info
    96  // and dial options.
    97  func DialInfo(info Info, opts DialOpts) (*mgo.DialInfo, error) {
    98  	if len(info.Addrs) == 0 {
    99  		return nil, stderrors.New("no mongo addresses")
   100  	}
   101  	if len(info.CACert) == 0 {
   102  		return nil, stderrors.New("missing CA certificate")
   103  	}
   104  	xcert, err := cert.ParseCert(info.CACert)
   105  	if err != nil {
   106  		return nil, fmt.Errorf("cannot parse CA certificate: %v", err)
   107  	}
   108  	pool := x509.NewCertPool()
   109  	pool.AddCert(xcert)
   110  	tlsConfig := &tls.Config{
   111  		RootCAs:    pool,
   112  		ServerName: "juju-mongodb",
   113  	}
   114  	dial := func(addr net.Addr) (net.Conn, error) {
   115  		c, err := net.Dial("tcp", addr.String())
   116  		if err != nil {
   117  			logger.Debugf("connection failed, will retry: %v", err)
   118  			return nil, err
   119  		}
   120  		cc := tls.Client(c, tlsConfig)
   121  		if err := cc.Handshake(); err != nil {
   122  			logger.Debugf("TLS handshake failed: %v", err)
   123  			return nil, err
   124  		}
   125  		logger.Infof("dialled mongo successfully on address %q", addr)
   126  		return cc, nil
   127  	}
   128  
   129  	return &mgo.DialInfo{
   130  		Addrs:   info.Addrs,
   131  		Timeout: opts.Timeout,
   132  		Dial:    dial,
   133  		Direct:  opts.Direct,
   134  	}, nil
   135  }
   136  
   137  // DialWithInfo establishes a new session to the cluster identified by info,
   138  // with the specified options.
   139  func DialWithInfo(info Info, opts DialOpts) (*mgo.Session, error) {
   140  	if opts.Timeout == 0 {
   141  		return nil, errors.New("a non-zero Timeout must be specified")
   142  	}
   143  
   144  	dialInfo, err := DialInfo(info, opts)
   145  	if err != nil {
   146  		return nil, err
   147  	}
   148  
   149  	session, err := mgo.DialWithInfo(dialInfo)
   150  	if err != nil {
   151  		return nil, err
   152  	}
   153  
   154  	if opts.SocketTimeout == 0 {
   155  		opts.SocketTimeout = SocketTimeout
   156  	}
   157  	session.SetSocketTimeout(opts.SocketTimeout)
   158  
   159  	if opts.PostDial != nil {
   160  		if err := opts.PostDial(session); err != nil {
   161  			session.Close()
   162  			return nil, errors.Annotate(err, "PostDial failed")
   163  		}
   164  	}
   165  	return session, nil
   166  }