github.com/wallyworld/juju@v0.0.0-20161013125918-6cf1bc9d917a/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/utils"
    16  	"gopkg.in/juju/names.v2"
    17  	"gopkg.in/mgo.v2"
    18  
    19  	"github.com/juju/juju/cert"
    20  )
    21  
    22  // SocketTimeout should be long enough that even a slow mongo server
    23  // will respond in that length of time, and must also be long enough
    24  // to allow for completion of heavyweight queries.
    25  //
    26  // Note: 1 minute is mgo's default socket timeout value.
    27  //
    28  // Also note: We have observed mongodb occasionally getting "stuck"
    29  // for over 30s in the field.
    30  const SocketTimeout = time.Minute
    31  
    32  // defaultDialTimeout should be representative of the upper bound of
    33  // time taken to dial a mongo server from within the same
    34  // cloud/private network.
    35  const defaultDialTimeout = 30 * time.Second
    36  
    37  // DialOpts holds configuration parameters that control the
    38  // Dialing behavior when connecting to a controller.
    39  type DialOpts struct {
    40  	// Timeout is the amount of time to wait contacting
    41  	// a controller.
    42  	Timeout time.Duration
    43  
    44  	// SocketTimeout is the amount of time to wait for a
    45  	// non-responding socket to the database before it is forcefully
    46  	// closed. If this is zero, the value of the SocketTimeout const
    47  	// will be used.
    48  	SocketTimeout time.Duration
    49  
    50  	// Direct informs whether to establish connections only with the
    51  	// specified seed servers, or to obtain information for the whole
    52  	// cluster and establish connections with further servers too.
    53  	Direct bool
    54  
    55  	// PostDial, if non-nil, is called by DialWithInfo with the
    56  	// mgo.Session after a successful dial but before DialWithInfo
    57  	// returns to its caller.
    58  	PostDial func(*mgo.Session) error
    59  }
    60  
    61  // DefaultDialOpts returns a DialOpts representing the default
    62  // parameters for contacting a controller.
    63  //
    64  // NOTE(axw) these options are inappropriate for tests in CI,
    65  // as CI tends to run on machines with slow I/O (or thrashed
    66  // I/O with limited IOPs). For tests, use mongotest.DialOpts().
    67  func DefaultDialOpts() DialOpts {
    68  	return DialOpts{
    69  		Timeout:       defaultDialTimeout,
    70  		SocketTimeout: SocketTimeout,
    71  	}
    72  }
    73  
    74  // Info encapsulates information about cluster of
    75  // mongo servers and can be used to make a
    76  // connection to that cluster.
    77  type Info struct {
    78  	// Addrs gives the addresses of the MongoDB servers for the state.
    79  	// Each address should be in the form address:port.
    80  	Addrs []string
    81  
    82  	// CACert holds the CA certificate that will be used
    83  	// to validate the controller's certificate, in PEM format.
    84  	CACert string
    85  }
    86  
    87  // MongoInfo encapsulates information about cluster of
    88  // servers holding juju state and can be used to make a
    89  // connection to that cluster.
    90  type MongoInfo struct {
    91  	// mongo.Info contains the addresses and cert of the mongo cluster.
    92  	Info
    93  	// Tag holds the name of the entity that is connecting.
    94  	// It should be nil when connecting as an administrator.
    95  	Tag names.Tag
    96  
    97  	// Password holds the password for the connecting entity.
    98  	Password string
    99  }
   100  
   101  // DialInfo returns information on how to dial
   102  // the state's mongo server with the given info
   103  // and dial options.
   104  func DialInfo(info Info, opts DialOpts) (*mgo.DialInfo, error) {
   105  	if len(info.Addrs) == 0 {
   106  		return nil, stderrors.New("no mongo addresses")
   107  	}
   108  	if len(info.CACert) == 0 {
   109  		return nil, stderrors.New("missing CA certificate")
   110  	}
   111  	xcert, err := cert.ParseCert(info.CACert)
   112  	if err != nil {
   113  		return nil, fmt.Errorf("cannot parse CA certificate: %v", err)
   114  	}
   115  	pool := x509.NewCertPool()
   116  	pool.AddCert(xcert)
   117  	tlsConfig := utils.SecureTLSConfig()
   118  
   119  	// TODO(natefinch): revisit this when are full-time on mongo 3.
   120  	// We have to add non-ECDHE suites because mongo doesn't support ECDHE.
   121  	moreSuites := []uint16{
   122  		tls.TLS_RSA_WITH_AES_128_GCM_SHA256,
   123  		tls.TLS_RSA_WITH_AES_256_GCM_SHA384,
   124  	}
   125  
   126  	tlsConfig.CipherSuites = append(tlsConfig.CipherSuites, moreSuites...)
   127  	tlsConfig.RootCAs = pool
   128  	tlsConfig.ServerName = "juju-mongodb"
   129  
   130  	dial := func(server *mgo.ServerAddr) (net.Conn, error) {
   131  		addr := server.TCPAddr().String()
   132  		c, err := net.DialTimeout("tcp", addr, opts.Timeout)
   133  		if err != nil {
   134  			logger.Warningf("mongodb connection failed, will retry: %v", err)
   135  			return nil, err
   136  		}
   137  		cc := tls.Client(c, tlsConfig)
   138  		if err := cc.Handshake(); err != nil {
   139  			logger.Warningf("TLS handshake failed: %v", err)
   140  			return nil, err
   141  		}
   142  		logger.Debugf("dialled mongodb server at %q", addr)
   143  		return cc, nil
   144  	}
   145  
   146  	return &mgo.DialInfo{
   147  		Addrs:      info.Addrs,
   148  		Timeout:    opts.Timeout,
   149  		DialServer: dial,
   150  		Direct:     opts.Direct,
   151  	}, nil
   152  }
   153  
   154  // DialWithInfo establishes a new session to the cluster identified by info,
   155  // with the specified options.
   156  func DialWithInfo(info Info, opts DialOpts) (*mgo.Session, error) {
   157  	if opts.Timeout == 0 {
   158  		return nil, errors.New("a non-zero Timeout must be specified")
   159  	}
   160  
   161  	dialInfo, err := DialInfo(info, opts)
   162  	if err != nil {
   163  		return nil, err
   164  	}
   165  
   166  	session, err := mgo.DialWithInfo(dialInfo)
   167  	if err != nil {
   168  		return nil, err
   169  	}
   170  
   171  	if opts.SocketTimeout == 0 {
   172  		opts.SocketTimeout = SocketTimeout
   173  	}
   174  	session.SetSocketTimeout(opts.SocketTimeout)
   175  
   176  	if opts.PostDial != nil {
   177  		if err := opts.PostDial(session); err != nil {
   178  			session.Close()
   179  			return nil, errors.Annotate(err, "PostDial failed")
   180  		}
   181  	}
   182  	return session, nil
   183  }