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  }