github.com/axw/juju@v0.0.0-20161005053422-4bd6544d08d4/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 func (c *JujuCommandBase) setCmdContext(ctx *cmd.Context) { 280 c.cmdContext = ctx 281 } 282 283 // apiOpen establishes a connection to the API server using the 284 // the give api.Info and api.DialOpts. 285 func (c *JujuCommandBase) apiOpen(info *api.Info, opts api.DialOpts) (api.Connection, error) { 286 if c.apiOpenFunc != nil { 287 return c.apiOpenFunc(info, opts) 288 } 289 return api.Open(info, opts) 290 } 291 292 // WrapBase wraps the specified CommandBase, returning a Command 293 // that proxies to each of the CommandBase methods. 294 func WrapBase(c CommandBase) cmd.Command { 295 return &baseCommandWrapper{ 296 CommandBase: c, 297 } 298 } 299 300 type baseCommandWrapper struct { 301 CommandBase 302 } 303 304 // Run implements Command.Run. 305 func (w *baseCommandWrapper) Run(ctx *cmd.Context) error { 306 defer w.closeContext() 307 w.setCmdContext(ctx) 308 return w.CommandBase.Run(ctx) 309 } 310 311 // SetFlags implements Command.SetFlags. 312 func (w *baseCommandWrapper) SetFlags(f *gnuflag.FlagSet) { 313 w.CommandBase.SetFlags(f) 314 } 315 316 // Init implements Command.Init. 317 func (w *baseCommandWrapper) Init(args []string) error { 318 return w.CommandBase.Init(args) 319 } 320 321 func newAPIConnectionParams( 322 store jujuclient.ClientStore, 323 controllerName, 324 modelName string, 325 accountDetails *jujuclient.AccountDetails, 326 bakery *httpbakery.Client, 327 apiOpen api.OpenFunc, 328 getPassword func(string) (string, error), 329 ) (juju.NewAPIConnectionParams, error) { 330 if controllerName == "" { 331 return juju.NewAPIConnectionParams{}, errors.Trace(errNoNameSpecified) 332 } 333 var modelUUID string 334 if modelName != "" { 335 modelDetails, err := store.ModelByName(controllerName, modelName) 336 if err != nil { 337 return juju.NewAPIConnectionParams{}, errors.Trace(err) 338 } 339 modelUUID = modelDetails.ModelUUID 340 } 341 dialOpts := api.DefaultDialOpts() 342 dialOpts.BakeryClient = bakery 343 344 if accountDetails != nil { 345 bakery.WebPageVisitor = httpbakery.NewMultiVisitor( 346 authentication.NewVisitor(accountDetails.User, getPassword), 347 bakery.WebPageVisitor, 348 ) 349 } 350 351 return juju.NewAPIConnectionParams{ 352 Store: store, 353 ControllerName: controllerName, 354 AccountDetails: accountDetails, 355 ModelUUID: modelUUID, 356 DialOpts: dialOpts, 357 OpenAPI: apiOpen, 358 }, nil 359 } 360 361 // NewGetBootstrapConfigParamsFunc returns a function that, given a controller name, 362 // returns the params needed to bootstrap a fresh copy of that controller in the given client store. 363 func NewGetBootstrapConfigParamsFunc(ctx *cmd.Context, store jujuclient.ClientStore) func(string) (*jujuclient.BootstrapConfig, *environs.PrepareConfigParams, error) { 364 return bootstrapConfigGetter{ctx, store}.getBootstrapConfigParams 365 } 366 367 type bootstrapConfigGetter struct { 368 ctx *cmd.Context 369 store jujuclient.ClientStore 370 } 371 372 func (g bootstrapConfigGetter) getBootstrapConfig(controllerName string) (*config.Config, error) { 373 bootstrapConfig, params, err := g.getBootstrapConfigParams(controllerName) 374 if err != nil { 375 return nil, errors.Trace(err) 376 } 377 provider, err := environs.Provider(bootstrapConfig.CloudType) 378 if err != nil { 379 return nil, errors.Trace(err) 380 } 381 return provider.PrepareConfig(*params) 382 } 383 384 func (g bootstrapConfigGetter) getBootstrapConfigParams(controllerName string) (*jujuclient.BootstrapConfig, *environs.PrepareConfigParams, error) { 385 if _, err := g.store.ControllerByName(controllerName); err != nil { 386 return nil, nil, errors.Annotate(err, "resolving controller name") 387 } 388 bootstrapConfig, err := g.store.BootstrapConfigForController(controllerName) 389 if err != nil { 390 return nil, nil, errors.Annotate(err, "getting bootstrap config") 391 } 392 393 var credential *cloud.Credential 394 if bootstrapConfig.Credential != "" { 395 bootstrapCloud := cloud.Cloud{ 396 Type: bootstrapConfig.CloudType, 397 Endpoint: bootstrapConfig.CloudEndpoint, 398 IdentityEndpoint: bootstrapConfig.CloudIdentityEndpoint, 399 } 400 if bootstrapConfig.CloudRegion != "" { 401 bootstrapCloud.Regions = []cloud.Region{{ 402 Name: bootstrapConfig.CloudRegion, 403 Endpoint: bootstrapConfig.CloudEndpoint, 404 IdentityEndpoint: bootstrapConfig.CloudIdentityEndpoint, 405 }} 406 } 407 credential, _, _, err = GetCredentials( 408 g.ctx, g.store, 409 GetCredentialsParams{ 410 Cloud: bootstrapCloud, 411 CloudName: bootstrapConfig.Cloud, 412 CloudRegion: bootstrapConfig.CloudRegion, 413 CredentialName: bootstrapConfig.Credential, 414 }, 415 ) 416 if err != nil { 417 return nil, nil, errors.Trace(err) 418 } 419 } else { 420 // The credential was auto-detected; run auto-detection again. 421 cloudCredential, err := DetectCredential( 422 bootstrapConfig.Cloud, 423 bootstrapConfig.CloudType, 424 ) 425 if err != nil { 426 return nil, nil, errors.Trace(err) 427 } 428 // DetectCredential ensures that there is only one credential 429 // to choose from. It's still in a map, though, hence for..range. 430 for _, one := range cloudCredential.AuthCredentials { 431 credential = &one 432 } 433 } 434 435 // Add attributes from the controller details. 436 controllerDetails, err := g.store.ControllerByName(controllerName) 437 if err != nil { 438 return nil, nil, errors.Trace(err) 439 } 440 441 // TODO(wallyworld) - remove after beta18 442 controllerModelUUID := bootstrapConfig.ControllerModelUUID 443 if controllerModelUUID == "" { 444 controllerModelUUID = controllerDetails.ControllerUUID 445 } 446 447 bootstrapConfig.Config[config.UUIDKey] = controllerModelUUID 448 cfg, err := config.New(config.NoDefaults, bootstrapConfig.Config) 449 if err != nil { 450 return nil, nil, errors.Trace(err) 451 } 452 return bootstrapConfig, &environs.PrepareConfigParams{ 453 environs.CloudSpec{ 454 bootstrapConfig.CloudType, 455 bootstrapConfig.Cloud, 456 bootstrapConfig.CloudRegion, 457 bootstrapConfig.CloudEndpoint, 458 bootstrapConfig.CloudIdentityEndpoint, 459 bootstrapConfig.CloudStorageEndpoint, 460 credential, 461 }, 462 cfg, 463 }, nil 464 } 465 466 // TODO(axw) this is now in three places: change-password, 467 // register, and here. Refactor and move to a common location. 468 func readPassword(stdin io.Reader) (string, error) { 469 if f, ok := stdin.(*os.File); ok && terminal.IsTerminal(int(f.Fd())) { 470 password, err := terminal.ReadPassword(int(f.Fd())) 471 return string(password), err 472 } 473 return readLine(stdin) 474 } 475 476 func readLine(stdin io.Reader) (string, error) { 477 // Read one byte at a time to avoid reading beyond the delimiter. 478 line, err := bufio.NewReader(byteAtATimeReader{stdin}).ReadString('\n') 479 if err != nil { 480 return "", errors.Trace(err) 481 } 482 return line[:len(line)-1], nil 483 } 484 485 type byteAtATimeReader struct { 486 io.Reader 487 } 488 489 // Read is part of the io.Reader interface. 490 func (r byteAtATimeReader) Read(out []byte) (int, error) { 491 return r.Reader.Read(out[:1]) 492 }