github.com/niedbalski/juju@v0.0.0-20190215020005-8ff100488e47/cmd/juju/controller/listmodels.go (about)

     1  // Copyright 2015 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package controller
     5  
     6  import (
     7  	"fmt"
     8  	"io"
     9  	"strings"
    10  	"time"
    11  
    12  	"github.com/juju/ansiterm"
    13  	"github.com/juju/cmd"
    14  	"github.com/juju/errors"
    15  	"github.com/juju/gnuflag"
    16  	"gopkg.in/juju/names.v2"
    17  
    18  	"github.com/juju/juju/api/base"
    19  	"github.com/juju/juju/apiserver/params"
    20  	jujucmd "github.com/juju/juju/cmd"
    21  	"github.com/juju/juju/cmd/juju/common"
    22  	"github.com/juju/juju/cmd/modelcmd"
    23  	"github.com/juju/juju/cmd/output"
    24  	"github.com/juju/juju/core/model"
    25  	"github.com/juju/juju/jujuclient"
    26  )
    27  
    28  // NewListModelsCommand returns a command to list models.
    29  func NewListModelsCommand() cmd.Command {
    30  	return modelcmd.WrapController(&modelsCommand{})
    31  }
    32  
    33  // ModelManagerAPI defines the methods on the model manager API that
    34  // the models command calls.
    35  type ModelManagerAPI interface {
    36  	Close() error
    37  	ListModels(user string) ([]base.UserModel, error)
    38  	ListModelSummaries(user string, all bool) ([]base.UserModelSummary, error)
    39  	ModelInfo([]names.ModelTag) ([]params.ModelInfoResult, error)
    40  	BestAPIVersion() int
    41  }
    42  
    43  // modelsCommand returns the list of all the models the
    44  // current user can access on the current controller.
    45  type modelsCommand struct {
    46  	modelcmd.ControllerCommandBase
    47  	out          cmd.Output
    48  	all          bool
    49  	loggedInUser string
    50  	user         string
    51  	listUUID     bool
    52  	exactTime    bool
    53  	modelAPI     ModelManagerAPI
    54  	sysAPI       ModelsSysAPI
    55  
    56  	runVars modelsRunValues
    57  }
    58  
    59  // Info implements Command.Info
    60  func (c *modelsCommand) Info() *cmd.Info {
    61  	return jujucmd.Info(&cmd.Info{
    62  		Name:    "models",
    63  		Purpose: "Lists models a user can access on a controller.",
    64  		Doc:     listModelsDoc,
    65  		Aliases: []string{"list-models"},
    66  	})
    67  }
    68  
    69  // SetFlags implements Command.SetFlags.
    70  func (c *modelsCommand) SetFlags(f *gnuflag.FlagSet) {
    71  	c.ControllerCommandBase.SetFlags(f)
    72  	f.StringVar(&c.user, "user", "", "The user to list models for (administrative users only)")
    73  	f.BoolVar(&c.all, "all", false, "Lists all models, regardless of user accessibility (administrative users only)")
    74  	f.BoolVar(&c.listUUID, "uuid", false, "Display UUID for models")
    75  	f.BoolVar(&c.exactTime, "exact-time", false, "Use full timestamps")
    76  	c.out.AddFlags(f, "tabular", map[string]cmd.Formatter{
    77  		"yaml":    cmd.FormatYaml,
    78  		"json":    cmd.FormatJson,
    79  		"tabular": c.formatTabular,
    80  	})
    81  }
    82  
    83  // Run implements Command.Run
    84  func (c *modelsCommand) Run(ctx *cmd.Context) error {
    85  	controllerName, err := c.ControllerName()
    86  	if err != nil {
    87  		ctx.Infof(err.Error())
    88  		return errors.Trace(err)
    89  	}
    90  	accountDetails, err := c.CurrentAccountDetails()
    91  	if err != nil {
    92  		ctx.Infof(err.Error())
    93  		return err
    94  	}
    95  	c.loggedInUser = accountDetails.User
    96  
    97  	if c.user == "" {
    98  		c.user = accountDetails.User
    99  	}
   100  	if !names.IsValidUser(c.user) {
   101  		err := errors.NotValidf("user %q", c.user)
   102  		ctx.Infof(err.Error())
   103  		return err
   104  	}
   105  
   106  	c.runVars = modelsRunValues{
   107  		currentUser:    names.NewUserTag(c.user),
   108  		controllerName: controllerName,
   109  	}
   110  	// TODO(perrito666) 2016-05-02 lp:1558657
   111  	now := time.Now()
   112  
   113  	modelmanagerAPI, err := c.getModelManagerAPI()
   114  	if err != nil {
   115  		ctx.Infof(err.Error())
   116  		return errors.Trace(err)
   117  	}
   118  	defer modelmanagerAPI.Close()
   119  
   120  	haveModels := false
   121  	if modelmanagerAPI.BestAPIVersion() > 3 {
   122  		haveModels, err = c.getModelSummaries(ctx, modelmanagerAPI, now)
   123  		if err != nil {
   124  			// This is needed to provide a consistent behavior with previous
   125  			// 'models' implementation.
   126  			err = errors.Annotate(err, "cannot list models")
   127  		}
   128  	} else {
   129  		haveModels, err = c.oldModelsCommandBehaviour(ctx, modelmanagerAPI, now)
   130  	}
   131  	if err != nil {
   132  		ctx.Infof(err.Error())
   133  		return err
   134  	}
   135  	if !haveModels && c.out.Name() == "tabular" {
   136  		// When the output is tabular, we inform the user when there
   137  		// are no models available, and tell them how to go about
   138  		// creating or granting access to them.
   139  		fmt.Fprintln(ctx.Stderr, noModelsMessage)
   140  	}
   141  	return nil
   142  }
   143  
   144  func (c *modelsCommand) currentModelName() (qualified, name string) {
   145  	current, err := c.ClientStore().CurrentModel(c.runVars.controllerName)
   146  	if err == nil {
   147  		qualified, name = current, current
   148  		if c.user != "" {
   149  			unqualifiedModelName, owner, err := jujuclient.SplitModelName(current)
   150  			if err == nil {
   151  				// If current model's owner is this user, un-qualify model name.
   152  				name = common.OwnerQualifiedModelName(
   153  					unqualifiedModelName, owner, c.runVars.currentUser,
   154  				)
   155  			}
   156  		}
   157  	}
   158  	return
   159  }
   160  
   161  func (c *modelsCommand) getModelManagerAPI() (ModelManagerAPI, error) {
   162  	if c.modelAPI != nil {
   163  		return c.modelAPI, nil
   164  	}
   165  	return c.NewModelManagerAPIClient()
   166  }
   167  
   168  func (c *modelsCommand) getModelSummaries(ctx *cmd.Context, client ModelManagerAPI, now time.Time) (bool, error) {
   169  	results, err := client.ListModelSummaries(c.user, c.all)
   170  	if err != nil {
   171  		return false, errors.Trace(err)
   172  	}
   173  	summaries := []ModelSummary{}
   174  	modelsToStore := map[string]jujuclient.ModelDetails{}
   175  	for _, result := range results {
   176  		// Since we do not want to throw away all results if we have an
   177  		// an issue with a model, we will display errors in Stderr
   178  		// and will continue processing the rest.
   179  		if result.Error != nil {
   180  			ctx.Infof(result.Error.Error())
   181  			continue
   182  		}
   183  		model, err := c.modelSummaryFromParams(result, now)
   184  		if err != nil {
   185  			ctx.Infof(err.Error())
   186  			continue
   187  		}
   188  		model.ControllerName = c.runVars.controllerName
   189  		summaries = append(summaries, model)
   190  		modelsToStore[model.Name] = jujuclient.ModelDetails{ModelUUID: model.UUID, ModelType: model.Type}
   191  	}
   192  	found := len(summaries) > 0
   193  
   194  	if err := c.ClientStore().SetModels(c.runVars.controllerName, modelsToStore); err != nil {
   195  		return found, errors.Trace(err)
   196  	}
   197  
   198  	// Identifying current model has to be done after models in client store have been updated
   199  	// since that call determines/updates current model information.
   200  	modelSummarySet := ModelSummarySet{Models: summaries}
   201  	modelSummarySet.CurrentModelQualified, modelSummarySet.CurrentModel = c.currentModelName()
   202  	if err := c.out.Write(ctx, modelSummarySet); err != nil {
   203  		return found, err
   204  	}
   205  	return found, err
   206  }
   207  
   208  // ModelSummarySet contains the set of summaries for models.
   209  type ModelSummarySet struct {
   210  	Models []ModelSummary `yaml:"models" json:"models"`
   211  
   212  	// CurrentModel is the name of the current model, qualified for the
   213  	// user for which we're listing models. i.e. for the user admin,
   214  	// and the model admin/foo, this field will contain "foo"; for
   215  	// bob and the same model, the field will contain "admin/foo".
   216  	CurrentModel string `yaml:"current-model,omitempty" json:"current-model,omitempty"`
   217  
   218  	// CurrentModelQualified is the fully qualified name for the current
   219  	// model, i.e. having the format $owner/$model.
   220  	CurrentModelQualified string `yaml:"-" json:"-"`
   221  }
   222  
   223  // ModelSummary contains a summary of some information about a model.
   224  type ModelSummary struct {
   225  	// Name is a fully qualified model name, i.e. having the format $owner/$model.
   226  	Name string `json:"name" yaml:"name"`
   227  
   228  	// ShortName is un-qualified model name.
   229  	ShortName string          `json:"short-name" yaml:"short-name"`
   230  	UUID      string          `json:"model-uuid" yaml:"model-uuid"`
   231  	Type      model.ModelType `json:"model-type" yaml:"model-type"`
   232  
   233  	ControllerUUID     string                  `json:"controller-uuid" yaml:"controller-uuid"`
   234  	ControllerName     string                  `json:"controller-name" yaml:"controller-name"`
   235  	IsController       bool                    `json:"is-controller" yaml:"is-controller"`
   236  	Owner              string                  `json:"owner" yaml:"owner"`
   237  	Cloud              string                  `json:"cloud" yaml:"cloud"`
   238  	CloudRegion        string                  `json:"region,omitempty" yaml:"region,omitempty"`
   239  	CloudCredential    *common.ModelCredential `json:"credential,omitempty" yaml:"credential,omitempty"`
   240  	ProviderType       string                  `json:"type,omitempty" yaml:"type,omitempty"`
   241  	Life               string                  `json:"life" yaml:"life"`
   242  	Status             *common.ModelStatus     `json:"status,omitempty" yaml:"status,omitempty"`
   243  	UserAccess         string                  `yaml:"access" json:"access"`
   244  	UserLastConnection string                  `yaml:"last-connection" json:"last-connection"`
   245  
   246  	// Counts is the map of different counts where key is the entity that was counted
   247  	// and value is the number, for e.g. {"machines":10,"cores":3}.
   248  	Counts       map[string]int64 `json:"-" yaml:"-"`
   249  	SLA          string           `json:"sla,omitempty" yaml:"sla,omitempty"`
   250  	SLAOwner     string           `json:"sla-owner,omitempty" yaml:"sla-owner,omitempty"`
   251  	AgentVersion string           `json:"agent-version,omitempty" yaml:"agent-version,omitempty"`
   252  }
   253  
   254  func (c *modelsCommand) modelSummaryFromParams(apiSummary base.UserModelSummary, now time.Time) (ModelSummary, error) {
   255  	summary := ModelSummary{
   256  		ShortName:      apiSummary.Name,
   257  		Name:           jujuclient.JoinOwnerModelName(names.NewUserTag(apiSummary.Owner), apiSummary.Name),
   258  		UUID:           apiSummary.UUID,
   259  		Type:           apiSummary.Type,
   260  		ControllerUUID: apiSummary.ControllerUUID,
   261  		IsController:   apiSummary.IsController,
   262  		Owner:          apiSummary.Owner,
   263  		Life:           apiSummary.Life,
   264  		Cloud:          apiSummary.Cloud,
   265  		CloudRegion:    apiSummary.CloudRegion,
   266  		UserAccess:     apiSummary.ModelUserAccess,
   267  		Status: &common.ModelStatus{
   268  			Current: apiSummary.Status.Status,
   269  			Message: apiSummary.Status.Info,
   270  			Since:   common.FriendlyDuration(apiSummary.Status.Since, now),
   271  		},
   272  	}
   273  	if apiSummary.AgentVersion != nil {
   274  		summary.AgentVersion = apiSummary.AgentVersion.String()
   275  	}
   276  	if apiSummary.Migration != nil {
   277  		status := summary.Status
   278  		if status == nil {
   279  			status = &common.ModelStatus{}
   280  			summary.Status = status
   281  		}
   282  		status.Migration = apiSummary.Migration.Status
   283  		status.MigrationStart = common.FriendlyDuration(apiSummary.Migration.StartTime, now)
   284  		status.MigrationEnd = common.FriendlyDuration(apiSummary.Migration.EndTime, now)
   285  	}
   286  
   287  	if apiSummary.ProviderType != "" {
   288  		summary.ProviderType = apiSummary.ProviderType
   289  
   290  	}
   291  	if apiSummary.CloudCredential != "" {
   292  		credTag := names.NewCloudCredentialTag(apiSummary.CloudCredential)
   293  		summary.CloudCredential = &common.ModelCredential{
   294  			Name:  credTag.Name(),
   295  			Owner: credTag.Owner().Id(),
   296  			Cloud: credTag.Cloud().Id(),
   297  		}
   298  	}
   299  	if apiSummary.UserLastConnection != nil {
   300  		summary.UserLastConnection = common.UserFriendlyDuration(*apiSummary.UserLastConnection, now)
   301  	} else {
   302  		summary.UserLastConnection = "never connected"
   303  	}
   304  	if apiSummary.SLA != nil {
   305  		summary.SLA = apiSummary.SLA.Level
   306  		summary.SLAOwner = apiSummary.SLA.Owner
   307  	}
   308  	summary.Counts = map[string]int64{}
   309  	for _, v := range apiSummary.Counts {
   310  		summary.Counts[v.Entity] = v.Count
   311  	}
   312  
   313  	// If hasMachinesCounts is not yet set, check if we should set it based on this model summary.
   314  	if !c.runVars.hasMachinesCount {
   315  		if _, ok := summary.Counts[string(params.Machines)]; ok {
   316  			c.runVars.hasMachinesCount = true
   317  		}
   318  	}
   319  
   320  	// If hasCoresCounts is not yet set, check if we should set it based on this model summary.
   321  	if !c.runVars.hasCoresCount {
   322  		if _, ok := summary.Counts[string(params.Cores)]; ok {
   323  			c.runVars.hasCoresCount = true
   324  		}
   325  	}
   326  	return summary, nil
   327  }
   328  
   329  // These values are specific to an individual Run() of the model command.
   330  type modelsRunValues struct {
   331  	currentUser      names.UserTag
   332  	controllerName   string
   333  	hasMachinesCount bool
   334  	hasCoresCount    bool
   335  }
   336  
   337  // formatTabular takes an interface{} to adhere to the cmd.Formatter interface
   338  func (c *modelsCommand) formatTabular(writer io.Writer, value interface{}) error {
   339  	summariesSet, ok := value.(ModelSummarySet)
   340  	if !ok {
   341  		modelSet, k := value.(ModelSet)
   342  		if !k {
   343  			return errors.Errorf("expected value of type ModelSummarySet or ModelSet, got %T", value)
   344  		}
   345  		return c.tabularSet(writer, modelSet)
   346  	}
   347  	return c.tabularSummaries(writer, summariesSet)
   348  }
   349  
   350  func (c *modelsCommand) tabularColumns(tw *ansiterm.TabWriter, w output.Wrapper) {
   351  	w.Println("Controller: " + c.runVars.controllerName)
   352  	w.Println()
   353  	w.Print("Model")
   354  	if c.listUUID {
   355  		w.Print("UUID")
   356  	}
   357  	w.Print("Cloud/Region", "Type", "Status")
   358  	printColumnHeader := func(columnName string, columnNumber int) {
   359  		w.Print(columnName)
   360  		offset := 0
   361  		if c.listUUID {
   362  			offset++
   363  		}
   364  		tw.SetColumnAlignRight(columnNumber + offset)
   365  	}
   366  
   367  	if c.runVars.hasMachinesCount {
   368  		printColumnHeader("Machines", 4)
   369  	}
   370  
   371  	if c.runVars.hasCoresCount {
   372  		printColumnHeader("Cores", 5)
   373  	}
   374  	w.Println("Access", "Last connection")
   375  }
   376  
   377  // tabularSummaries takes model summaries set to adhere to the cmd.Formatter interface
   378  func (c *modelsCommand) tabularSummaries(writer io.Writer, modelSet ModelSummarySet) error {
   379  	tw := output.TabWriter(writer)
   380  	w := output.Wrapper{tw}
   381  	c.tabularColumns(tw, w)
   382  
   383  	for _, model := range modelSet.Models {
   384  		cloudRegion := strings.Trim(model.Cloud+"/"+model.CloudRegion, "/")
   385  		owner := names.NewUserTag(model.Owner)
   386  		name := model.Name
   387  		if c.runVars.currentUser == owner {
   388  			// No need to display fully qualified model name to its owner.
   389  			name = model.ShortName
   390  		}
   391  		if model.Name == modelSet.CurrentModelQualified {
   392  			name += "*"
   393  			w.PrintColor(output.CurrentHighlight, name)
   394  		} else {
   395  			w.Print(name)
   396  		}
   397  		if c.listUUID {
   398  			w.Print(model.UUID)
   399  		}
   400  		status := "-"
   401  		if model.Status != nil && model.Status.Current.String() != "" {
   402  			status = model.Status.Current.String()
   403  		}
   404  		w.Print(cloudRegion, model.ProviderType, status)
   405  		if c.runVars.hasMachinesCount {
   406  			if v, ok := model.Counts[string(params.Machines)]; ok {
   407  				w.Print(v)
   408  			} else {
   409  				w.Print(0)
   410  			}
   411  		}
   412  		if c.runVars.hasCoresCount {
   413  			if v, ok := model.Counts[string(params.Cores)]; ok {
   414  				w.Print(v)
   415  			} else {
   416  				w.Print("-")
   417  			}
   418  		}
   419  		access := model.UserAccess
   420  		if access == "" {
   421  			access = "-"
   422  		}
   423  		w.Println(access, model.UserLastConnection)
   424  	}
   425  	tw.Flush()
   426  	return nil
   427  }
   428  
   429  var listModelsDoc = `
   430  The models listed here are either models you have created yourself, or
   431  models which have been shared with you. Default values for user and
   432  controller are, respectively, the current user and the current controller.
   433  The active model is denoted by an asterisk.
   434  
   435  Examples:
   436  
   437      juju models
   438      juju models --user bob
   439  
   440  See also:
   441      add-model
   442      share-model
   443      unshare-model
   444  `