github.com/cloudbase/juju-core@v0.0.0-20140504232958-a7271ac7912f/juju/api.go (about) 1 // Copyright 2013 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 4 package juju 5 6 import ( 7 "fmt" 8 "io" 9 "time" 10 11 "github.com/juju/loggo" 12 13 "launchpad.net/juju-core/environs" 14 "launchpad.net/juju-core/environs/config" 15 "launchpad.net/juju-core/environs/configstore" 16 "launchpad.net/juju-core/errors" 17 "launchpad.net/juju-core/juju/osenv" 18 "launchpad.net/juju-core/names" 19 "launchpad.net/juju-core/state/api" 20 "launchpad.net/juju-core/state/api/keymanager" 21 "launchpad.net/juju-core/utils/parallel" 22 ) 23 24 var logger = loggo.GetLogger("juju") 25 26 // The following are variables so that they can be 27 // changed by tests. 28 var ( 29 apiOpen = api.Open 30 apiClose = (*api.State).Close 31 providerConnectDelay = 2 * time.Second 32 ) 33 34 // apiState wraps an api.State, redefining its Close method 35 // so we can abuse it for testing purposes. 36 type apiState struct { 37 st *api.State 38 // If cachedInfo is non-nil, it indicates that the info has been 39 // newly retrieved, and should be cached in the config store. 40 cachedInfo *api.Info 41 } 42 43 func (st apiState) Close() error { 44 return apiClose(st.st) 45 } 46 47 // APIConn holds a connection to a juju environment and its 48 // associated state through its API interface. 49 type APIConn struct { 50 Environ environs.Environ 51 State *api.State 52 } 53 54 var errAborted = fmt.Errorf("aborted") 55 56 // NewAPIConn returns a new Conn that uses the 57 // given environment. The environment must have already 58 // been bootstrapped. 59 func NewAPIConn(environ environs.Environ, dialOpts api.DialOpts) (*APIConn, error) { 60 info, err := environAPIInfo(environ) 61 if err != nil { 62 return nil, err 63 } 64 65 st, err := apiOpen(info, dialOpts) 66 // TODO(rog): handle errUnauthorized when the API handles passwords. 67 if err != nil { 68 return nil, err 69 } 70 return &APIConn{ 71 Environ: environ, 72 State: st, 73 }, nil 74 } 75 76 // Close terminates the connection to the environment and releases 77 // any associated resources. 78 func (c *APIConn) Close() error { 79 return apiClose(c.State) 80 } 81 82 // NewAPIClientFromName returns an api.Client connected to the API Server for 83 // the named environment. If envName is "", the default environment 84 // will be used. 85 func NewAPIClientFromName(envName string) (*api.Client, error) { 86 st, err := newAPIClient(envName) 87 if err != nil { 88 return nil, err 89 } 90 return st.Client(), nil 91 } 92 93 // NewKeyManagerClient returns an api.keymanager.Client connected to the API Server for 94 // the named environment. If envName is "", the default environment will be used. 95 func NewKeyManagerClient(envName string) (*keymanager.Client, error) { 96 st, err := newAPIClient(envName) 97 if err != nil { 98 return nil, err 99 } 100 return keymanager.NewClient(st), nil 101 } 102 103 func newAPIClient(envName string) (*api.State, error) { 104 store, err := configstore.NewDisk(osenv.JujuHome()) 105 if err != nil { 106 return nil, err 107 } 108 return newAPIFromName(envName, store) 109 } 110 111 // newAPIFromName implements the bulk of NewAPIClientFromName 112 // but is separate for testing purposes. 113 func newAPIFromName(envName string, store configstore.Storage) (*api.State, error) { 114 // Try to read the default environment configuration file. 115 // If it doesn't exist, we carry on in case 116 // there's some environment info for that environment. 117 // This enables people to copy environment files 118 // into their .juju/environments directory and have 119 // them be directly useful with no further configuration changes. 120 envs, err := environs.ReadEnvirons("") 121 if err == nil { 122 if envName == "" { 123 envName = envs.Default 124 } 125 if envName == "" { 126 return nil, fmt.Errorf("no default environment found") 127 } 128 } else if !environs.IsNoEnv(err) { 129 return nil, err 130 } 131 132 // Try to connect to the API concurrently using two different 133 // possible sources of truth for the API endpoint. Our 134 // preference is for the API endpoint cached in the API info, 135 // because we know that without needing to access any remote 136 // provider. However, the addresses stored there may no longer 137 // be current (and the network connection may take a very long 138 // time to time out) so we also try to connect using information 139 // found from the provider. We only start to make that 140 // connection after some suitable delay, so that in the 141 // hopefully usual case, we will make the connection to the API 142 // and never hit the provider. By preference we use provider 143 // attributes from the config store, but for backward 144 // compatibility reasons, we fall back to information from 145 // ReadEnvirons if that does not exist. 146 chooseError := func(err0, err1 error) error { 147 if err0 == nil { 148 return err1 149 } 150 if errorImportance(err0) < errorImportance(err1) { 151 err0, err1 = err1, err0 152 } 153 logger.Warningf("discarding API open error: %v", err1) 154 return err0 155 } 156 try := parallel.NewTry(0, chooseError) 157 158 info, err := store.ReadInfo(envName) 159 if err != nil && !errors.IsNotFoundError(err) { 160 return nil, err 161 } 162 var delay time.Duration 163 if info != nil && len(info.APIEndpoint().Addresses) > 0 { 164 logger.Debugf("trying cached API connection settings") 165 try.Start(func(stop <-chan struct{}) (io.Closer, error) { 166 return apiInfoConnect(store, info, stop) 167 }) 168 // Delay the config connection until we've spent 169 // some time trying to connect to the cached info. 170 delay = providerConnectDelay 171 } else { 172 logger.Debugf("no cached API connection settings found") 173 } 174 try.Start(func(stop <-chan struct{}) (io.Closer, error) { 175 return apiConfigConnect(info, envs, envName, stop, delay) 176 }) 177 try.Close() 178 val0, err := try.Result() 179 if err != nil { 180 if ierr, ok := err.(*infoConnectError); ok { 181 // lose error encapsulation: 182 err = ierr.error 183 } 184 return nil, err 185 } 186 val := val0.(apiState) 187 188 if val.cachedInfo != nil && info != nil { 189 // Cache the connection settings only if we used the 190 // environment config, but any errors are just logged 191 // as warnings, because they're not fatal. 192 err = cacheAPIInfo(info, val.cachedInfo) 193 if err != nil { 194 logger.Warningf(err.Error()) 195 } else { 196 logger.Debugf("updated API connection settings cache") 197 } 198 } 199 return val.st, nil 200 } 201 202 func errorImportance(err error) int { 203 if err == nil { 204 return 0 205 } 206 if errors.IsNotFoundError(err) { 207 // An error from an actual connection attempt 208 // is more interesting than the fact that there's 209 // no environment info available. 210 return 1 211 } 212 if _, ok := err.(*infoConnectError); ok { 213 // A connection to a potentially stale cached address 214 // is less important than a connection from fresh info. 215 return 2 216 } 217 return 3 218 } 219 220 type infoConnectError struct { 221 error 222 } 223 224 // apiInfoConnect looks for endpoint on the given environment and 225 // tries to connect to it, sending the result on the returned channel. 226 func apiInfoConnect(store configstore.Storage, info configstore.EnvironInfo, stop <-chan struct{}) (apiState, error) { 227 endpoint := info.APIEndpoint() 228 if info == nil || len(endpoint.Addresses) == 0 { 229 return apiState{}, &infoConnectError{fmt.Errorf("no cached addresses")} 230 } 231 logger.Infof("connecting to API addresses: %v", endpoint.Addresses) 232 apiInfo := &api.Info{ 233 Addrs: endpoint.Addresses, 234 CACert: []byte(endpoint.CACert), 235 Tag: names.UserTag(info.APICredentials().User), 236 Password: info.APICredentials().Password, 237 } 238 st, err := apiOpen(apiInfo, api.DefaultDialOpts()) 239 if err != nil { 240 return apiState{}, &infoConnectError{err} 241 } 242 return apiState{st, nil}, err 243 } 244 245 // apiConfigConnect looks for configuration info on the given environment, 246 // and tries to use an Environ constructed from that to connect to 247 // its endpoint. It only starts the attempt after the given delay, 248 // to allow the faster apiInfoConnect to hopefully succeed first. 249 // It returns nil if there was no configuration information found. 250 func apiConfigConnect(info configstore.EnvironInfo, envs *environs.Environs, envName string, stop <-chan struct{}, delay time.Duration) (apiState, error) { 251 var cfg *config.Config 252 var err error 253 if info != nil && len(info.BootstrapConfig()) > 0 { 254 cfg, err = config.New(config.NoDefaults, info.BootstrapConfig()) 255 } else if envs != nil { 256 cfg, err = envs.Config(envName) 257 if errors.IsNotFoundError(err) { 258 return apiState{}, err 259 } 260 } else { 261 return apiState{}, errors.NotFoundf("environment %q", envName) 262 } 263 select { 264 case <-time.After(delay): 265 case <-stop: 266 return apiState{}, errAborted 267 } 268 environ, err := environs.New(cfg) 269 if err != nil { 270 return apiState{}, err 271 } 272 apiInfo, err := environAPIInfo(environ) 273 if err != nil { 274 return apiState{}, err 275 } 276 st, err := apiOpen(apiInfo, api.DefaultDialOpts()) 277 // TODO(rog): handle errUnauthorized when the API handles passwords. 278 if err != nil { 279 return apiState{}, err 280 } 281 return apiState{st, apiInfo}, nil 282 } 283 284 func environAPIInfo(environ environs.Environ) (*api.Info, error) { 285 _, info, err := environ.StateInfo() 286 if err != nil { 287 return nil, err 288 } 289 info.Tag = "user-admin" 290 password := environ.Config().AdminSecret() 291 if password == "" { 292 return nil, fmt.Errorf("cannot connect without admin-secret") 293 } 294 info.Password = password 295 return info, nil 296 } 297 298 // cacheAPIInfo updates the local environment settings (.jenv file) 299 // with the provided apiInfo, assuming we've just successfully 300 // connected to the API server. 301 func cacheAPIInfo(info configstore.EnvironInfo, apiInfo *api.Info) error { 302 info.SetAPIEndpoint(configstore.APIEndpoint{ 303 Addresses: apiInfo.Addrs, 304 CACert: string(apiInfo.CACert), 305 }) 306 _, username, err := names.ParseTag(apiInfo.Tag, names.UserTagKind) 307 if err != nil { 308 return fmt.Errorf("not caching API connection settings: invalid API user tag: %v", err) 309 } 310 info.SetAPICredentials(configstore.APICredentials{ 311 User: username, 312 Password: apiInfo.Password, 313 }) 314 if err := info.Write(); err != nil { 315 return fmt.Errorf("cannot cache API connection settings: %v", err) 316 } 317 return nil 318 }