github.com/wallyworld/juju@v0.0.0-20161013125918-6cf1bc9d917a/cmd/modelcmd/base.go (about) 1 // Copyright 2015 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 4 package modelcmd 5 6 import ( 7 "bufio" 8 "fmt" 9 "io" 10 "net/http" 11 "os" 12 13 "github.com/juju/cmd" 14 "github.com/juju/errors" 15 "github.com/juju/gnuflag" 16 "golang.org/x/crypto/ssh/terminal" 17 "gopkg.in/juju/names.v2" 18 "gopkg.in/macaroon-bakery.v1/httpbakery" 19 20 "github.com/juju/juju/api" 21 "github.com/juju/juju/api/authentication" 22 "github.com/juju/juju/api/base" 23 "github.com/juju/juju/api/modelmanager" 24 "github.com/juju/juju/apiserver/params" 25 "github.com/juju/juju/cloud" 26 "github.com/juju/juju/environs" 27 "github.com/juju/juju/environs/config" 28 "github.com/juju/juju/juju" 29 "github.com/juju/juju/jujuclient" 30 ) 31 32 var errNoNameSpecified = errors.New("no name specified") 33 34 // CommandBase extends cmd.Command with a closeContext method. 35 // It is implicitly implemented by any type that embeds JujuCommandBase. 36 type CommandBase interface { 37 cmd.Command 38 39 // closeContext closes the command's API context. 40 closeContext() 41 setCmdContext(*cmd.Context) 42 } 43 44 // ModelAPI provides access to the model client facade methods. 45 type ModelAPI interface { 46 ListModels(user string) ([]base.UserModel, error) 47 Close() error 48 } 49 50 // JujuCommandBase is a convenience type for embedding that need 51 // an API connection. 52 type JujuCommandBase struct { 53 cmd.CommandBase 54 cmdContext *cmd.Context 55 apiContext *APIContext 56 modelAPI_ ModelAPI 57 apiOpenFunc api.OpenFunc 58 authOpts AuthOpts 59 } 60 61 // closeContext closes the command's API context 62 // if it has actually been created. 63 func (c *JujuCommandBase) closeContext() { 64 if c.apiContext != nil { 65 if err := c.apiContext.Close(); err != nil { 66 logger.Errorf("%v", err) 67 } 68 } 69 } 70 71 // SetFlags implements cmd.Command.SetFlags. 72 func (c *JujuCommandBase) SetFlags(f *gnuflag.FlagSet) { 73 c.authOpts.SetFlags(f) 74 } 75 76 // SetModelAPI sets the api used to access model information. 77 func (c *JujuCommandBase) SetModelAPI(api ModelAPI) { 78 c.modelAPI_ = api 79 } 80 81 // SetAPIOpen sets the function used for opening an API connection. 82 func (c *JujuCommandBase) SetAPIOpen(apiOpen api.OpenFunc) { 83 c.apiOpenFunc = apiOpen 84 } 85 86 func (c *JujuCommandBase) modelAPI(store jujuclient.ClientStore, controllerName string) (ModelAPI, error) { 87 if c.modelAPI_ != nil { 88 return c.modelAPI_, nil 89 } 90 conn, err := c.NewAPIRoot(store, controllerName, "") 91 if err != nil { 92 return nil, errors.Trace(err) 93 } 94 c.modelAPI_ = modelmanager.NewClient(conn) 95 return c.modelAPI_, nil 96 } 97 98 // NewAPIRoot returns a new connection to the API server for the given 99 // model or controller. 100 func (c *JujuCommandBase) NewAPIRoot( 101 store jujuclient.ClientStore, 102 controllerName, modelName string, 103 ) (api.Connection, error) { 104 accountDetails, err := store.AccountDetails(controllerName) 105 if err != nil && !errors.IsNotFound(err) { 106 return nil, errors.Trace(err) 107 } 108 // If there are no account details or there's no logged-in 109 // user or the user is external, then trigger macaroon authentication 110 // by using an empty AccountDetails. 111 if accountDetails == nil || accountDetails.User == "" { 112 accountDetails = &jujuclient.AccountDetails{} 113 } else { 114 u := names.NewUserTag(accountDetails.User) 115 if !u.IsLocal() { 116 accountDetails = &jujuclient.AccountDetails{} 117 } 118 } 119 param, err := c.NewAPIConnectionParams( 120 store, controllerName, modelName, accountDetails, 121 ) 122 if err != nil { 123 return nil, errors.Trace(err) 124 } 125 conn, err := juju.NewAPIConnection(param) 126 if modelName != "" && params.ErrCode(err) == params.CodeModelNotFound { 127 return nil, c.missingModelError(store, controllerName, modelName) 128 } 129 return conn, err 130 } 131 132 func (c *JujuCommandBase) missingModelError(store jujuclient.ClientStore, controllerName, modelName string) error { 133 // First, we'll try and clean up the missing model from the local cache. 134 err := store.RemoveModel(controllerName, modelName) 135 if err != nil { 136 logger.Warningf("cannot remove unknown model from cache: %v", err) 137 } 138 currentModel, err := store.CurrentModel(controllerName) 139 if err != nil { 140 logger.Warningf("cannot read current model: %v", err) 141 } else if currentModel == modelName { 142 if err := store.SetCurrentModel(controllerName, ""); err != nil { 143 logger.Warningf("cannot reset current model: %v", err) 144 } 145 } 146 errorMessage := "model %q has been removed from the controller, run 'juju models' and switch to one of them." 147 modelInfoMessage := "\nThere are %d accessible models on controller %q." 148 models, err := store.AllModels(controllerName) 149 if err == nil { 150 modelInfoMessage = fmt.Sprintf(modelInfoMessage, len(models), controllerName) 151 } else { 152 modelInfoMessage = "" 153 } 154 return errors.Errorf(errorMessage+modelInfoMessage, modelName) 155 } 156 157 // NewAPIConnectionParams returns a juju.NewAPIConnectionParams with the 158 // given arguments such that a call to juju.NewAPIConnection with the 159 // result behaves the same as a call to JujuCommandBase.NewAPIRoot with 160 // the same arguments. 161 func (c *JujuCommandBase) NewAPIConnectionParams( 162 store jujuclient.ClientStore, 163 controllerName, modelName string, 164 accountDetails *jujuclient.AccountDetails, 165 ) (juju.NewAPIConnectionParams, error) { 166 bakeryClient, err := c.BakeryClient() 167 if err != nil { 168 return juju.NewAPIConnectionParams{}, errors.Trace(err) 169 } 170 var getPassword func(username string) (string, error) 171 if c.cmdContext != nil { 172 getPassword = func(username string) (string, error) { 173 fmt.Fprintf(c.cmdContext.Stderr, "please enter password for %s on %s: ", username, controllerName) 174 defer fmt.Fprintln(c.cmdContext.Stderr) 175 return readPassword(c.cmdContext.Stdin) 176 } 177 } else { 178 getPassword = func(username string) (string, error) { 179 return "", errors.New("no context to prompt for password") 180 } 181 } 182 183 return newAPIConnectionParams( 184 store, controllerName, modelName, 185 accountDetails, 186 bakeryClient, 187 c.apiOpen, 188 getPassword, 189 ) 190 } 191 192 // HTTPClient returns an http.Client that contains the loaded 193 // persistent cookie jar. Note that this client is not good for 194 // connecting to the Juju API itself because it does not 195 // have the correct TLS setup - use api.Connection.HTTPClient 196 // for that. 197 func (c *JujuCommandBase) HTTPClient() (*http.Client, error) { 198 bakeryClient, err := c.BakeryClient() 199 if err != nil { 200 return nil, errors.Trace(err) 201 } 202 return bakeryClient.Client, nil 203 } 204 205 // BakeryClient returns a macaroon bakery client that 206 // uses the same HTTP client returned by HTTPClient. 207 func (c *JujuCommandBase) BakeryClient() (*httpbakery.Client, error) { 208 if err := c.initAPIContext(); err != nil { 209 return nil, errors.Trace(err) 210 } 211 return c.apiContext.NewBakeryClient(), nil 212 } 213 214 // APIOpen establishes a connection to the API server using the 215 // the given api.Info and api.DialOpts. 216 func (c *JujuCommandBase) APIOpen(info *api.Info, opts api.DialOpts) (api.Connection, error) { 217 if err := c.initAPIContext(); err != nil { 218 return nil, errors.Trace(err) 219 } 220 return c.apiOpen(info, opts) 221 } 222 223 // RefreshModels refreshes the local models cache for the current user 224 // on the specified controller. 225 func (c *JujuCommandBase) RefreshModels(store jujuclient.ClientStore, controllerName string) error { 226 modelManager, err := c.modelAPI(store, controllerName) 227 if err != nil { 228 return errors.Trace(err) 229 } 230 defer modelManager.Close() 231 232 accountDetails, err := store.AccountDetails(controllerName) 233 if err != nil { 234 return errors.Trace(err) 235 } 236 237 models, err := modelManager.ListModels(accountDetails.User) 238 if err != nil { 239 return errors.Trace(err) 240 } 241 for _, model := range models { 242 modelDetails := jujuclient.ModelDetails{model.UUID} 243 owner := names.NewUserTag(model.Owner) 244 modelName := jujuclient.JoinOwnerModelName(owner, model.Name) 245 if err := store.UpdateModel(controllerName, modelName, modelDetails); err != nil { 246 return errors.Trace(err) 247 } 248 } 249 return nil 250 } 251 252 // initAPIContext lazily initializes c.apiContext. Doing this lazily means that 253 // we avoid unnecessarily loading and saving the cookies 254 // when a command does not actually make an API connection. 255 func (c *JujuCommandBase) initAPIContext() error { 256 if c.apiContext != nil { 257 return nil 258 } 259 apiContext, err := NewAPIContext(c.cmdContext, &c.authOpts) 260 if err != nil { 261 return errors.Trace(err) 262 } 263 c.apiContext = apiContext 264 return nil 265 } 266 267 // APIContext returns the API context used by the command. 268 // It should only be called while the Run method is being called. 269 // 270 // The returned APIContext should not be closed (it will be 271 // closed when the Run method completes). 272 func (c *JujuCommandBase) APIContext() (*APIContext, error) { 273 if err := c.initAPIContext(); err != nil { 274 return nil, errors.Trace(err) 275 } 276 return c.apiContext, nil 277 } 278 279 // ClearControllerMacaroons will remove all macaroons stored 280 // for the controller from the persistent cookie jar. 281 // This is called both from 'juju logout' and a failed 'juju register'. 282 func (c *JujuCommandBase) ClearControllerMacaroons(endpoints []string) error { 283 apictx, err := c.APIContext() 284 if err != nil { 285 return errors.Trace(err) 286 } 287 for _, s := range endpoints { 288 apictx.Jar.RemoveAllHost(s) 289 } 290 if err := apictx.Jar.Save(); err != nil { 291 return errors.Annotate(err, "can't remove cached authentication cookie") 292 } 293 return nil 294 } 295 296 func (c *JujuCommandBase) setCmdContext(ctx *cmd.Context) { 297 c.cmdContext = ctx 298 } 299 300 // apiOpen establishes a connection to the API server using the 301 // the give api.Info and api.DialOpts. 302 func (c *JujuCommandBase) apiOpen(info *api.Info, opts api.DialOpts) (api.Connection, error) { 303 if c.apiOpenFunc != nil { 304 return c.apiOpenFunc(info, opts) 305 } 306 return api.Open(info, opts) 307 } 308 309 // WrapBase wraps the specified CommandBase, returning a Command 310 // that proxies to each of the CommandBase methods. 311 func WrapBase(c CommandBase) cmd.Command { 312 return &baseCommandWrapper{ 313 CommandBase: c, 314 } 315 } 316 317 type baseCommandWrapper struct { 318 CommandBase 319 } 320 321 // Run implements Command.Run. 322 func (w *baseCommandWrapper) Run(ctx *cmd.Context) error { 323 defer w.closeContext() 324 w.setCmdContext(ctx) 325 return w.CommandBase.Run(ctx) 326 } 327 328 // SetFlags implements Command.SetFlags. 329 func (w *baseCommandWrapper) SetFlags(f *gnuflag.FlagSet) { 330 w.CommandBase.SetFlags(f) 331 } 332 333 // Init implements Command.Init. 334 func (w *baseCommandWrapper) Init(args []string) error { 335 return w.CommandBase.Init(args) 336 } 337 338 func newAPIConnectionParams( 339 store jujuclient.ClientStore, 340 controllerName, 341 modelName string, 342 accountDetails *jujuclient.AccountDetails, 343 bakery *httpbakery.Client, 344 apiOpen api.OpenFunc, 345 getPassword func(string) (string, error), 346 ) (juju.NewAPIConnectionParams, error) { 347 if controllerName == "" { 348 return juju.NewAPIConnectionParams{}, errors.Trace(errNoNameSpecified) 349 } 350 var modelUUID string 351 if modelName != "" { 352 modelDetails, err := store.ModelByName(controllerName, modelName) 353 if err != nil { 354 return juju.NewAPIConnectionParams{}, errors.Trace(err) 355 } 356 modelUUID = modelDetails.ModelUUID 357 } 358 dialOpts := api.DefaultDialOpts() 359 dialOpts.BakeryClient = bakery 360 361 if accountDetails != nil { 362 bakery.WebPageVisitor = httpbakery.NewMultiVisitor( 363 authentication.NewVisitor(accountDetails.User, getPassword), 364 bakery.WebPageVisitor, 365 ) 366 } 367 368 return juju.NewAPIConnectionParams{ 369 Store: store, 370 ControllerName: controllerName, 371 AccountDetails: accountDetails, 372 ModelUUID: modelUUID, 373 DialOpts: dialOpts, 374 OpenAPI: apiOpen, 375 }, nil 376 } 377 378 // NewGetBootstrapConfigParamsFunc returns a function that, given a controller name, 379 // returns the params needed to bootstrap a fresh copy of that controller in the given client store. 380 func NewGetBootstrapConfigParamsFunc(ctx *cmd.Context, store jujuclient.ClientStore) func(string) (*jujuclient.BootstrapConfig, *environs.PrepareConfigParams, error) { 381 return bootstrapConfigGetter{ctx, store}.getBootstrapConfigParams 382 } 383 384 type bootstrapConfigGetter struct { 385 ctx *cmd.Context 386 store jujuclient.ClientStore 387 } 388 389 func (g bootstrapConfigGetter) getBootstrapConfig(controllerName string) (*config.Config, error) { 390 bootstrapConfig, params, err := g.getBootstrapConfigParams(controllerName) 391 if err != nil { 392 return nil, errors.Trace(err) 393 } 394 provider, err := environs.Provider(bootstrapConfig.CloudType) 395 if err != nil { 396 return nil, errors.Trace(err) 397 } 398 return provider.PrepareConfig(*params) 399 } 400 401 func (g bootstrapConfigGetter) getBootstrapConfigParams(controllerName string) (*jujuclient.BootstrapConfig, *environs.PrepareConfigParams, error) { 402 if _, err := g.store.ControllerByName(controllerName); err != nil { 403 return nil, nil, errors.Annotate(err, "resolving controller name") 404 } 405 bootstrapConfig, err := g.store.BootstrapConfigForController(controllerName) 406 if err != nil { 407 return nil, nil, errors.Annotate(err, "getting bootstrap config") 408 } 409 410 var credential *cloud.Credential 411 if bootstrapConfig.Credential != "" { 412 bootstrapCloud := cloud.Cloud{ 413 Type: bootstrapConfig.CloudType, 414 Endpoint: bootstrapConfig.CloudEndpoint, 415 IdentityEndpoint: bootstrapConfig.CloudIdentityEndpoint, 416 } 417 if bootstrapConfig.CloudRegion != "" { 418 bootstrapCloud.Regions = []cloud.Region{{ 419 Name: bootstrapConfig.CloudRegion, 420 Endpoint: bootstrapConfig.CloudEndpoint, 421 IdentityEndpoint: bootstrapConfig.CloudIdentityEndpoint, 422 }} 423 } 424 credential, _, _, err = GetCredentials( 425 g.ctx, g.store, 426 GetCredentialsParams{ 427 Cloud: bootstrapCloud, 428 CloudName: bootstrapConfig.Cloud, 429 CloudRegion: bootstrapConfig.CloudRegion, 430 CredentialName: bootstrapConfig.Credential, 431 }, 432 ) 433 if err != nil { 434 return nil, nil, errors.Trace(err) 435 } 436 } else { 437 // The credential was auto-detected; run auto-detection again. 438 cloudCredential, err := DetectCredential( 439 bootstrapConfig.Cloud, 440 bootstrapConfig.CloudType, 441 ) 442 if err != nil { 443 return nil, nil, errors.Trace(err) 444 } 445 // DetectCredential ensures that there is only one credential 446 // to choose from. It's still in a map, though, hence for..range. 447 for _, one := range cloudCredential.AuthCredentials { 448 credential = &one 449 } 450 } 451 452 // Add attributes from the controller details. 453 controllerDetails, err := g.store.ControllerByName(controllerName) 454 if err != nil { 455 return nil, nil, errors.Trace(err) 456 } 457 458 // TODO(wallyworld) - remove after beta18 459 controllerModelUUID := bootstrapConfig.ControllerModelUUID 460 if controllerModelUUID == "" { 461 controllerModelUUID = controllerDetails.ControllerUUID 462 } 463 464 bootstrapConfig.Config[config.UUIDKey] = controllerModelUUID 465 cfg, err := config.New(config.NoDefaults, bootstrapConfig.Config) 466 if err != nil { 467 return nil, nil, errors.Trace(err) 468 } 469 return bootstrapConfig, &environs.PrepareConfigParams{ 470 environs.CloudSpec{ 471 bootstrapConfig.CloudType, 472 bootstrapConfig.Cloud, 473 bootstrapConfig.CloudRegion, 474 bootstrapConfig.CloudEndpoint, 475 bootstrapConfig.CloudIdentityEndpoint, 476 bootstrapConfig.CloudStorageEndpoint, 477 credential, 478 }, 479 cfg, 480 }, nil 481 } 482 483 // TODO(axw) this is now in three places: change-password, 484 // register, and here. Refactor and move to a common location. 485 func readPassword(stdin io.Reader) (string, error) { 486 if f, ok := stdin.(*os.File); ok && terminal.IsTerminal(int(f.Fd())) { 487 password, err := terminal.ReadPassword(int(f.Fd())) 488 return string(password), err 489 } 490 return readLine(stdin) 491 } 492 493 func readLine(stdin io.Reader) (string, error) { 494 // Read one byte at a time to avoid reading beyond the delimiter. 495 line, err := bufio.NewReader(byteAtATimeReader{stdin}).ReadString('\n') 496 if err != nil { 497 return "", errors.Trace(err) 498 } 499 return line[:len(line)-1], nil 500 } 501 502 type byteAtATimeReader struct { 503 io.Reader 504 } 505 506 // Read is part of the io.Reader interface. 507 func (r byteAtATimeReader) Read(out []byte) (int, error) { 508 return r.Reader.Read(out[:1]) 509 }