github.com/cloud-green/juju@v0.0.0-20151002100041-a00291338d3d/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 state server. 36 type DialOpts struct { 37 // Timeout is the amount of time to wait contacting 38 // a state server. 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 state server. 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 state server'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 }