github.com/axw/juju@v0.0.0-20161005053422-4bd6544d08d4/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(addr net.Addr) (net.Conn, error) { 131 c, err := net.Dial("tcp", addr.String()) 132 if err != nil { 133 logger.Debugf("connection failed, will retry: %v", err) 134 return nil, err 135 } 136 cc := tls.Client(c, tlsConfig) 137 if err := cc.Handshake(); err != nil { 138 logger.Debugf("TLS handshake failed: %v", err) 139 return nil, err 140 } 141 logger.Infof("dialled mongo successfully on address %q", addr) 142 return cc, nil 143 } 144 145 return &mgo.DialInfo{ 146 Addrs: info.Addrs, 147 Timeout: opts.Timeout, 148 Dial: dial, 149 Direct: opts.Direct, 150 }, nil 151 } 152 153 // DialWithInfo establishes a new session to the cluster identified by info, 154 // with the specified options. 155 func DialWithInfo(info Info, opts DialOpts) (*mgo.Session, error) { 156 if opts.Timeout == 0 { 157 return nil, errors.New("a non-zero Timeout must be specified") 158 } 159 160 dialInfo, err := DialInfo(info, opts) 161 if err != nil { 162 return nil, err 163 } 164 165 session, err := mgo.DialWithInfo(dialInfo) 166 if err != nil { 167 return nil, err 168 } 169 170 if opts.SocketTimeout == 0 { 171 opts.SocketTimeout = SocketTimeout 172 } 173 session.SetSocketTimeout(opts.SocketTimeout) 174 175 if opts.PostDial != nil { 176 if err := opts.PostDial(session); err != nil { 177 session.Close() 178 return nil, errors.Annotate(err, "PostDial failed") 179 } 180 } 181 return session, nil 182 }