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 `