github.com/niedbalski/juju@v0.0.0-20190215020005-8ff100488e47/cmd/juju/controller/showcontroller.go (about) 1 // Copyright 2016 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 4 package controller 5 6 import ( 7 "fmt" 8 "sync" 9 10 "github.com/juju/cmd" 11 "github.com/juju/errors" 12 "github.com/juju/gnuflag" 13 "gopkg.in/juju/names.v2" 14 15 "github.com/juju/juju/api/base" 16 "github.com/juju/juju/api/controller" 17 "github.com/juju/juju/apiserver/params" 18 jujucmd "github.com/juju/juju/cmd" 19 "github.com/juju/juju/cmd/modelcmd" 20 "github.com/juju/juju/core/status" 21 "github.com/juju/juju/environs/bootstrap" 22 "github.com/juju/juju/jujuclient" 23 "github.com/juju/juju/permission" 24 ) 25 26 var usageShowControllerSummary = ` 27 Shows detailed information of a controller.`[1:] 28 29 var usageShowControllerDetails = ` 30 Shows extended information about a controller(s) as well as related models 31 and user login details. 32 33 Examples: 34 juju show-controller 35 juju show-controller aws google 36 37 See also: 38 controllers`[1:] 39 40 type showControllerCommand struct { 41 modelcmd.CommandBase 42 43 out cmd.Output 44 store jujuclient.ClientStore 45 mu sync.Mutex 46 api func(controllerName string) ControllerAccessAPI 47 48 controllerNames []string 49 showPasswords bool 50 } 51 52 // NewShowControllerCommand returns a command to show details of the desired controllers. 53 func NewShowControllerCommand() cmd.Command { 54 cmd := &showControllerCommand{ 55 store: jujuclient.NewFileClientStore(), 56 } 57 return modelcmd.WrapBase(cmd) 58 } 59 60 // Init implements Command.Init. 61 func (c *showControllerCommand) Init(args []string) (err error) { 62 c.controllerNames = args 63 return nil 64 } 65 66 // Info implements Command.Info 67 func (c *showControllerCommand) Info() *cmd.Info { 68 return jujucmd.Info(&cmd.Info{ 69 Name: "show-controller", 70 Args: "[<controller name> ...]", 71 Purpose: usageShowControllerSummary, 72 Doc: usageShowControllerDetails, 73 }) 74 } 75 76 // SetFlags implements Command.SetFlags. 77 func (c *showControllerCommand) SetFlags(f *gnuflag.FlagSet) { 78 c.CommandBase.SetFlags(f) 79 f.BoolVar(&c.showPasswords, "show-password", false, "Show password for logged in user") 80 c.out.AddFlags(f, "yaml", map[string]cmd.Formatter{ 81 "yaml": cmd.FormatYaml, 82 "json": cmd.FormatJson, 83 }) 84 } 85 86 // ControllerAccessAPI defines a subset of the api/controller/Client API. 87 type ControllerAccessAPI interface { 88 GetControllerAccess(user string) (permission.Access, error) 89 ModelConfig() (map[string]interface{}, error) 90 ModelStatus(models ...names.ModelTag) ([]base.ModelStatus, error) 91 AllModels() ([]base.UserModel, error) 92 MongoVersion() (string, error) 93 IdentityProviderURL() (string, error) 94 Close() error 95 } 96 97 func (c *showControllerCommand) getAPI(controllerName string) (ControllerAccessAPI, error) { 98 if c.api != nil { 99 return c.api(controllerName), nil 100 } 101 api, err := c.NewAPIRoot(c.store, controllerName, "") 102 if err != nil { 103 return nil, errors.Annotate(err, "opening API connection") 104 } 105 return controller.NewClient(api), nil 106 } 107 108 // Run implements Command.Run 109 func (c *showControllerCommand) Run(ctx *cmd.Context) error { 110 controllerNames := c.controllerNames 111 if len(controllerNames) == 0 { 112 currentController, err := c.store.CurrentController() 113 if errors.IsNotFound(err) { 114 return errors.New("there is no active controller") 115 } else if err != nil { 116 return errors.Trace(err) 117 } 118 controllerNames = []string{currentController} 119 } 120 controllers := make(map[string]ShowControllerDetails) 121 c.mu.Lock() 122 defer c.mu.Unlock() 123 for _, controllerName := range controllerNames { 124 one, err := c.store.ControllerByName(controllerName) 125 if err != nil { 126 return err 127 } 128 var access string 129 client, err := c.getAPI(controllerName) 130 if err != nil { 131 return err 132 } 133 defer client.Close() 134 accountDetails, err := c.store.AccountDetails(controllerName) 135 if err != nil { 136 fmt.Fprintln(ctx.Stderr, err) 137 access = "(error)" 138 } else { 139 access = c.userAccess(client, ctx, accountDetails.User) 140 one.AgentVersion = c.agentVersion(client, ctx) 141 } 142 143 var ( 144 details ShowControllerDetails 145 allModels []base.UserModel 146 mongoVersion string 147 ) 148 149 // NOTE: this user may have been granted AddModelAccess which 150 // should allow them to list only the models they have access to. 151 // However, the code that grants permissions currently uses an 152 // escape hatch (to be removed in juju 3) that actually grants 153 // controller cloud access instead of controller access. 154 // 155 // The side-effect to this is that the userAccess() call above 156 // will return LoginAccess even if the user has been granted 157 // AddModelAccess causing the calls in the block below to fail 158 // with a permission error. As a workaround, unless the user 159 // has Superuser access we default to an empty model list which 160 // allows us to display non-model controller details. 161 if permission.Access(access).EqualOrGreaterControllerAccessThan(permission.SuperuserAccess) { 162 if allModels, err = client.AllModels(); err != nil { 163 details.Errors = append(details.Errors, err.Error()) 164 continue 165 } 166 // Update client store. 167 if err := c.SetControllerModels(c.store, controllerName, allModels); err != nil { 168 details.Errors = append(details.Errors, err.Error()) 169 continue 170 } 171 172 // Fetch mongoVersion if the apiserver supports it 173 mongoVersion, err = client.MongoVersion() 174 if err != nil && !errors.IsNotSupported(err) { 175 details.Errors = append(details.Errors, err.Error()) 176 continue 177 } 178 } 179 180 // Fetch identityURL if the apiserver supports it 181 identityURL, err := client.IdentityProviderURL() 182 if err != nil && !errors.IsNotSupported(err) { 183 details.Errors = append(details.Errors, err.Error()) 184 continue 185 } 186 187 modelTags := make([]names.ModelTag, len(allModels)) 188 var controllerModelUUID string 189 for i, m := range allModels { 190 modelTags[i] = names.NewModelTag(m.UUID) 191 if m.Name == bootstrap.ControllerModelName { 192 controllerModelUUID = m.UUID 193 } 194 } 195 modelStatusResults, err := client.ModelStatus(modelTags...) 196 if err != nil { 197 details.Errors = append(details.Errors, err.Error()) 198 continue 199 } 200 201 c.convertControllerForShow(&details, controllerName, one, access, allModels, modelStatusResults, mongoVersion, identityURL) 202 controllers[controllerName] = details 203 machineCount := 0 204 for _, r := range modelStatusResults { 205 if r.Error != nil { 206 if !errors.IsNotFound(r.Error) { 207 details.Errors = append(details.Errors, r.Error.Error()) 208 } 209 continue 210 } 211 machineCount += r.TotalMachineCount 212 } 213 one.MachineCount = &machineCount 214 one.ActiveControllerMachineCount, one.ControllerMachineCount = ControllerMachineCounts(controllerModelUUID, modelStatusResults) 215 err = c.store.UpdateController(controllerName, *one) 216 if err != nil { 217 details.Errors = append(details.Errors, err.Error()) 218 } 219 } 220 return c.out.Write(ctx, controllers) 221 } 222 223 func (c *showControllerCommand) userAccess(client ControllerAccessAPI, ctx *cmd.Context, user string) string { 224 var access string 225 userAccess, err := client.GetControllerAccess(user) 226 if err == nil { 227 access = string(userAccess) 228 } else { 229 code := params.ErrCode(err) 230 if code != "" { 231 access = fmt.Sprintf("(%s)", code) 232 } else { 233 fmt.Fprintln(ctx.Stderr, err) 234 access = "(error)" 235 } 236 } 237 return access 238 } 239 240 func (c *showControllerCommand) agentVersion(client ControllerAccessAPI, ctx *cmd.Context) string { 241 var ver string 242 mc, err := client.ModelConfig() 243 if err != nil { 244 code := params.ErrCode(err) 245 if code != "" { 246 ver = fmt.Sprintf("(%s)", code) 247 } else { 248 fmt.Fprintln(ctx.Stderr, err) 249 ver = "(error)" 250 } 251 return ver 252 } 253 return mc["agent-version"].(string) 254 } 255 256 type ShowControllerDetails struct { 257 // Details contains the same details that client store caches for this controller. 258 Details ControllerDetails `yaml:"details,omitempty" json:"details,omitempty"` 259 260 // Machines is a collection of all machines forming the controller cluster. 261 Machines map[string]MachineDetails `yaml:"controller-machines,omitempty" json:"controller-machines,omitempty"` 262 263 // Models is a collection of all models for this controller. 264 Models map[string]ModelDetails `yaml:"models,omitempty" json:"models,omitempty"` 265 266 // CurrentModel is the name of the current model for this controller 267 CurrentModel string `yaml:"current-model,omitempty" json:"current-model,omitempty"` 268 269 // Account is the account details for the user logged into this controller. 270 Account *AccountDetails `yaml:"account,omitempty" json:"account,omitempty"` 271 272 // Errors is a collection of errors related to accessing this controller details. 273 Errors []string `yaml:"errors,omitempty" json:"errors,omitempty"` 274 } 275 276 // ControllerDetails holds details of a controller to show. 277 type ControllerDetails struct { 278 // TODO(anastasiamac 2018-08-10) This is a deprecated property, see lp#1596607. 279 // It was added for backward compatibility, lp#1786061, to be removed for Juju 3. 280 OldControllerUUID string `yaml:"uuid" json:"-"` 281 282 // ControllerUUID is the unique ID for the controller. 283 ControllerUUID string `yaml:"controller-uuid" json:"uuid"` 284 285 // APIEndpoints is the collection of API endpoints running in this controller. 286 APIEndpoints []string `yaml:"api-endpoints,flow" json:"api-endpoints"` 287 288 // CACert is a security certificate for this controller. 289 CACert string `yaml:"ca-cert" json:"ca-cert"` 290 291 // Cloud is the name of the cloud that this controller runs in. 292 Cloud string `yaml:"cloud" json:"cloud"` 293 294 // CloudRegion is the name of the cloud region that this controller runs in. 295 CloudRegion string `yaml:"region,omitempty" json:"region,omitempty"` 296 297 // AgentVersion is the version of the agent running on this controller. 298 // AgentVersion need not always exist so we omitempty here. This struct is 299 // used in both list-controller and show-controller. show-controller 300 // displays the agent version where list-controller does not. 301 AgentVersion string `yaml:"agent-version,omitempty" json:"agent-version,omitempty"` 302 303 // MongoVersion is the version of the mongo server running on this 304 // controller. 305 MongoVersion string `yaml:"mongo-version,omitempty" json:"mongo-version,omitempty"` 306 307 // IdentityURL contails the address of an external identity provider 308 // if one has been configured for this controller. 309 IdentityURL string `yaml:"identity-url,omitempty" json:"identity-url,omitempty"` 310 } 311 312 // ModelDetails holds details of a model to show. 313 type MachineDetails struct { 314 // ID holds the id of the machine. 315 ID string `yaml:"id,omitempty" json:"id,omitempty"` 316 317 // InstanceID holds the cloud instance id of the machine. 318 InstanceID string `yaml:"instance-id,omitempty" json:"instance-id,omitempty"` 319 320 // HAStatus holds information informing of the HA status of the machine. 321 HAStatus string `yaml:"ha-status,omitempty" json:"ha-status,omitempty"` 322 } 323 324 // ModelDetails holds details of a model to show. 325 type ModelDetails struct { 326 // TODO(anastasiamac 2018-08-10) This is a deprecated property, see lp#1596607. 327 // It was added for backward compatibility, lp#1786061, to be removed for Juju 3. 328 OldModelUUID string `yaml:"uuid" json:"-"` 329 330 // ModelUUID holds the details of a model. 331 ModelUUID string `yaml:"model-uuid" json:"uuid"` 332 333 // MachineCount holds the number of machines in the model. 334 MachineCount *int `yaml:"machine-count,omitempty" json:"machine-count,omitempty"` 335 336 // CoreCount holds the number of cores across the machines in the model. 337 CoreCount *int `yaml:"core-count,omitempty" json:"core-count,omitempty"` 338 } 339 340 // AccountDetails holds details of an account to show. 341 type AccountDetails struct { 342 // User is the username for the account. 343 User string `yaml:"user" json:"user"` 344 345 // Access is the level of access the user has on the controller. 346 Access string `yaml:"access,omitempty" json:"access,omitempty"` 347 348 // Password is the password for the account. 349 Password string `yaml:"password,omitempty" json:"password,omitempty"` 350 } 351 352 func (c *showControllerCommand) convertControllerForShow( 353 controller *ShowControllerDetails, 354 controllerName string, 355 details *jujuclient.ControllerDetails, 356 access string, 357 allModels []base.UserModel, 358 modelStatusResults []base.ModelStatus, 359 mongoVersion string, 360 identityURL string, 361 ) { 362 363 controller.Details = ControllerDetails{ 364 ControllerUUID: details.ControllerUUID, 365 OldControllerUUID: details.ControllerUUID, 366 APIEndpoints: details.APIEndpoints, 367 CACert: details.CACert, 368 Cloud: details.Cloud, 369 CloudRegion: details.CloudRegion, 370 AgentVersion: details.AgentVersion, 371 MongoVersion: mongoVersion, 372 IdentityURL: identityURL, 373 } 374 c.convertModelsForShow(controllerName, controller, allModels, modelStatusResults) 375 c.convertAccountsForShow(controllerName, controller, access) 376 var controllerModelUUID string 377 for _, m := range allModels { 378 if m.Name == bootstrap.ControllerModelName { 379 controllerModelUUID = m.UUID 380 break 381 } 382 } 383 if controllerModelUUID != "" { 384 var controllerModel base.ModelStatus 385 found := false 386 for _, m := range modelStatusResults { 387 if m.Error != nil { 388 // This most likely occurred because a model was 389 // destroyed half-way through the call. 390 continue 391 } 392 if m.UUID == controllerModelUUID { 393 controllerModel = m 394 found = true 395 break 396 } 397 } 398 if found { 399 c.convertMachinesForShow(controllerName, controller, controllerModel) 400 } 401 } 402 } 403 404 func (c *showControllerCommand) convertAccountsForShow(controllerName string, controller *ShowControllerDetails, access string) { 405 storeDetails, err := c.store.AccountDetails(controllerName) 406 if err != nil && !errors.IsNotFound(err) { 407 controller.Errors = append(controller.Errors, err.Error()) 408 } 409 if storeDetails == nil { 410 return 411 } 412 details := &AccountDetails{ 413 User: storeDetails.User, 414 Access: access, 415 } 416 if c.showPasswords { 417 details.Password = storeDetails.Password 418 } 419 controller.Account = details 420 } 421 422 func (c *showControllerCommand) convertModelsForShow( 423 controllerName string, 424 controller *ShowControllerDetails, 425 models []base.UserModel, 426 modelStatus []base.ModelStatus, 427 ) { 428 controller.Models = make(map[string]ModelDetails) 429 for i, model := range models { 430 modelDetails := ModelDetails{ModelUUID: model.UUID, OldModelUUID: model.UUID} 431 result := modelStatus[i] 432 if result.Error != nil { 433 if !errors.IsNotFound(result.Error) { 434 controller.Errors = append(controller.Errors, errors.Annotatef(result.Error, "model uuid %v", model.UUID).Error()) 435 } 436 } else { 437 if result.TotalMachineCount > 0 { 438 modelDetails.MachineCount = new(int) 439 *modelDetails.MachineCount = result.TotalMachineCount 440 } 441 if result.CoreCount > 0 { 442 modelDetails.CoreCount = new(int) 443 *modelDetails.CoreCount = result.CoreCount 444 } 445 } 446 controller.Models[model.Name] = modelDetails 447 } 448 var err error 449 controller.CurrentModel, err = c.store.CurrentModel(controllerName) 450 if err != nil && !errors.IsNotFound(err) { 451 controller.Errors = append(controller.Errors, err.Error()) 452 } 453 } 454 455 func (c *showControllerCommand) convertMachinesForShow( 456 controllerName string, 457 controller *ShowControllerDetails, 458 controllerModel base.ModelStatus, 459 ) { 460 controller.Machines = make(map[string]MachineDetails) 461 numControllers := 0 462 for _, m := range controllerModel.Machines { 463 if !m.WantsVote { 464 continue 465 } 466 numControllers++ 467 } 468 for _, m := range controllerModel.Machines { 469 if !m.WantsVote { 470 // Skip non controller machines. 471 continue 472 } 473 instId := m.InstanceId 474 if instId == "" { 475 instId = "(unprovisioned)" 476 } 477 details := MachineDetails{InstanceID: instId} 478 if numControllers > 1 { 479 details.HAStatus = haStatus(m.HasVote, m.WantsVote, m.Status) 480 } 481 controller.Machines[m.Id] = details 482 } 483 } 484 485 func haStatus(hasVote bool, wantsVote bool, statusStr string) string { 486 if statusStr == string(status.Down) { 487 return "down, lost connection" 488 } 489 if !wantsVote { 490 return "" 491 } 492 if hasVote { 493 return "ha-enabled" 494 } 495 return "ha-pending" 496 }