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  }