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  }