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 }